Moneydance Developer Resources
Extending Moneydance
Moneydance enables advanced users to develop extensions using a free Developer Kit that is easy to download and use. The kit includes sample code, the required libraries for packaging and signing extensions, and a Gradle wrapper script for compiling, packaging, and signing them.
Extensions can be written in Kotlin or Java and run on the Java Virtual Machine (JVM).
Python scripts written in Jython 2.7 can also be executed. Refer to the Moneydance Python Developer Resources page for more information.
Kotlin (or Java) is generally recommended for new extensions. Python is ideal for ad hoc scripting and automation.
The Moneydance API that can be used from your extension code is available here: Moneydance Core API
Download a copy of the Developer Kit (version 6.0): Download Developer Kit
Visit Infinite Kind’s Open source repository and reference implementations for actual code examples of real extensions
Developer Support
An additional resource is the Extension Development section of The Infinite Kind’s support forum. This forum can be used for questions about writing extensions, scripts, and code that works with Moneydance.
Background Information
Each extension is self-contained in a single MXT file (essentially a special jar file, which is a special zip file)
A correctly packaged extension is completely contained within an MXT file. Moneydance can add extensions that are registered in the official directory which can be accessed under the Extensions -> Manage Extensions menu in the app. The Extension Developer Kit can be used to package and sign your extension.
Every extension includes a digital signature that is verified each time the extension is loaded
Every extension must be audited and signed by The Infinite Kind before users will be able to load it into Moneydance. Extensions with invalid or missing signatures will generate an error and will not be loaded unless the user forces the extension to load.
Extensions can access the financial data objects within Moneydance
Extensions have full access to the financial data in Moneydance. They can analyze, edit, and modify the data. Extensions are also notified when application events happen such as opening or closing a file, or modifying a transaction.
Extensions can integrate with the Moneydance user interface to provide a seamless user experience
Extensions can:
- register “features” which add menu items to the main toolbar’s Extensions menu (these invoke the registered extension when clicked).
- be notified when the user opens or closes a file, or whenever any part of the data model is modified.
- also register and display light-weight widgets on the home / summary screen.
Quick Start (5 minutes)
-
Download the Developer Kit and extract it to a folder of your choice.
-
Open a Terminal and change into the extracted folder.
-
Generate the bundled sample extension
m yextension:./gradlew clean genkeys myextension -
Locate the generated extension:
dist/myextension.mxt -
Install it in Moneydance using Extensions → Manage Extensions → Add from File…, then select
myextension.mxt.
That’s it — the sample extension is now installed and ready to use. To create your own extension, duplicate or rename the myextension folder using a new name (which also becomes the extension ID), update the references to the new ID and name throughout the project, update the build.gradle file, then build it.
Extension entry point and registering a feature
Every extension’s entry point is a class that extends FeatureModule. Moneydance calls this class’s init() method during application startup (assuming the extension is installed), and calls invoke(String uri) when the user activates the extension (for example from the Extensions menu).
class Main : FeatureModule() { // com.moneydance.apps.md.controller.FeatureModule
private var mdMain:com.moneydance.apps.md.controller.Main? = null
private var extensionContext:FeatureModuleContext? = null // com.moneydance.apps.md.controller.FeatureModuleContext
override fun init() {
extensionContext = context // obtain reference to extension's own context - use getContext() in java
mdMain = extensionContext as com.moneydance.apps.md.controller.Main // upcast for full Moneydance capabilities
val mdGUI = mdMain.ui!! as MoneydanceGUI // obtain a reference to the Moneydance GUI - com.moneydance.apps.md.view.gui.MoneydanceGUI
extensionContext?.registerFeature(this, "doSomethingCool", null, getName()) // register on the extensions menu
}
override fun getName(): String = "Extension's Name"
override fun invoke(uri: String) {} // triggered by user selecting item on the extensions menu
override fun handleEvent(appEvent: String) {} // handle events triggered by Moneydance
override fun cleanup() { unload() } // always pass on to unload()
override fun unload() {} // please release resources when the data file is closed
}
| Method | Called when | Notes |
|---|---|---|
init() |
Once, called by Moneydance at application startup, if the extension is installed, or upon module install | Obtain your context reference here; call registerFeature()/registerHomePageView() if needed. Note: the GUI and dataset are not loaded yet at MD startup (unless this is happening during an install/reinstall, in which case the GUI is available) |
getName() |
Whenever Moneydance needs to display your extension’s name | e.g. on the Extensions menu |
invoke(uri: String) |
User selects your extension from the Extensions menu, or something invokes your registered URI via showURL() |
You can also call this yourself internally to trigger your own actions |
handleEvent(appEvent: String) |
Moneydance fires an application event (md:file:opened, etc.) |
See Handling events for the full event list |
getActionsForContext(context) (since MD2024 build 5100) |
Every time a right-click context menu is being constructed | Must return quickly; return an empty list if nothing applies |
cleanup() |
Never actually called by Moneydance - to be safe, forward on to unload() |
Always pass through to unload() |
unload() |
Extension is being uninstalled, reinstalled, or Moneydance is shutting down | Release all references to data objects here |
Adding context menu (right-click) items
available since Moneydance 2024 (build 5100)
To add items to Moneydance’s right-click context menus, override getActionsForContext() in your FeatureModule. It is called each time a context menu is constructed and should return quickly — the default implementation returns an empty list, and yours should bail out early wherever there’s nothing relevant to add.
context is an MDActionContext, which tells you where the right-click context menu action occurred and what’s selected — context.type (an ActionContextType, e.g. register, invest_register, home_search), context.accounts, and context.items (which you can filter to the transaction types you care about, e.g. AbstractTxn).
Each item in the returned list is a standard javax.swing.Action. Moneydance provides MDAction as a lightweight way to build one from a label, a command string, and a callback:
Only build and add the action if it’s actually applicable to the current selection — e.g. only add/show a context menu item when more than one transaction is selected:
override fun getActionsForContext(context: MDActionContext): List<Action> {
val actions = mutableListOf<Action>()
val listAccts = context.accounts
val listTxns = context.items.filterIsInstance<AbstractTxn>()
if (listTxns.isEmpty() && listAccts.isEmpty()) return actions
if (context.type == ActionContextType.register && listTxns.size > 1) { actions += makeValueSelectedAction(listTxns) }
return actions
}
private fun makeValueSelectedAction(listTxns: List<AbstractTxn>): Action {
return MDAction.make("Do something cool on selected transactions").command("do_something").callback { doSomethingWithThese(listTxns) }
}
Note: from Java, the same properties are context.getType(), context.getAccounts(), and context.getItems().
Registering a home page view
Extensions can add light-weight widgets to the home/summary page. To do this, call: getContext().registerHomePageView(this, view)
where view is an object implementing the HomePageView interface:
Implementations should stay lightweight until getGUIView(AccountBook) is called, since HomePageView objects may be constructed without ever being displayed.
Key points:
- The GUI component (
view) should be built lazily on first call togetGUIView(AccountBook), not in the constructor. getID()must return a unique identifier for your home page view.refresh()is called to refresh the viewsetActive()is where listeners (account changes, currency changes, etc.) should be added or removed, so the view doesn’t hold references or fire updates while off-screen.reset()tears the view down completely and clears the reference, ready for a fresh build the next time a file is opened.
Coalescing frequent refresh() calls
Ideally, you shouldn’t rebuild the view directly in refresh() if it may be called rapidly (e.g. by listeners). Consider wrapping it in a CollapsibleRefresher, which coalesces bursts of calls into a single update on the EDT:
class MyHomePageView : HomePageView {
private var view: javax.swing.JLabel? = null
override fun getID(): String = "myextension_homepage"
override fun toString(): String = "My Extension"
override fun getGUIView(book: AccountBook): javax.swing.JComponent {
if (view == null) { view = JLabel("Hello from My Extension") }
return view!!
}
private val refresher = CollapsibleRefresher { reallyRefresh() }
override fun refresh() { refresher.enqueueRefresh() }
private fun reallyRefresh() { view?.text = "Hello from My Extension" }
override fun setActive(active: Boolean) {}
override fun reset() { view = null }
}
- Full API reference:
HomePageView
Invoking other Moneydance features and extensions using URIs
Extensions can invoke other features and extensions in Moneydance using Uniform Resource Identifiers (URIs), which identify different resources and services. For example, an extension could call the following to display a net worth report:
getContext().showURL("moneydance:showreport:networthrpt");
or the following to display the reminders management window:
getContext().showURL("moneydance:remindershome");
URIs allow extensions to invoke almost any function available within Moneydance while maintaining loose coupling with the application.
For a list of URIs that can be invoked from within Moneydance see our URI Scheme page.
Logging to the Moneydance Console
To write diagnostic output to Help → Show Console, all extensions can write to the standard error stream:
System.err.println("Hello from my extension")
Since Moneydance 2024 (build 5100), the preferred approach is AppDebug, which provides structured logging and supports lazy evaluation so log messages are only constructed when the logger is enabled:
AppDebug.ALL.log("Hello from my extension") // always logs to the console
AppDebug.DEBUG.log { "WARNING from my extension" } // lazy; only logs when the DEBUG logger is enabled
Use the lambda form whenever constructing the message is potentially expensive, as it avoids the overhead when logging is disabled. AppDebug also supports logging exceptions and objects.
For maximum compatibility with older Moneydance releases, use System.err.println() (The AppDebug class will not exist on older versions of Moneydance)
Accessing the data model - entry points: accounts, currencies, transactions, reminders, reports, budgets
The Moneydance data model classes reside within: com.infinitekind.moneydance.model
All data within an open Moneydance file is accessed via the current AccountBook, obtained from your extension’s FeatureModuleContext:
val context = context!!
val book = context.currentAccountBook ?: return
val allAccounts = AccountUtil.allMatchesForSearch(book, AcctFilter.ALL_ACCOUNTS_FILTER)
val realAccounts = AccountUtil.allMatchesForSearch(book, AcctFilter.VIEWABLE_ACCOUNTS_FILTER)
val allCurrencies = book.currencies.allCurrencies.filter { it.currencyType == CurrencyType.Type.CURRENCY }
val allSecurities = book.currencies.allCurrencies.filter { it.currencyType == CurrencyType.Type.SECURITY }
val allTxns = book.transactionSet.allTxns
val allReminders = book.reminders.allReminders
val allReports = book.memorizedItems.allItems
val allBudgets = book.budgets.allBudgets
Note: the same applies from Java — each Kotlin property becomes its getter, e.g. book.getCurrencies(), book.getTransactionSet(), book.getReminders(), book.getMemorizedItems(), book.getBudgets(), and .getAllCurrencies()/.getAllTxns()/.getAllReminders()/.getAllItems()/.getAllBudgets().
Tip: Filtering accounts with AcctFilter
AcctFilter lets you filter accounts when scanning a data set, instead of manually walking the account tree. Implement matches(Account) to select the accounts you want, then pass it to AccountUtil.allMatchesForSearch(book, filter). Use the built-in AcctFilter.ALL_ACCOUNTS_FILTER when you don’t need to filter at all.
Updating data
Moneydance data objects can be updated by extensions. For example, your code could add a transaction, change an Account’s name, or add a price to a Security’s price history
- Once you have a reference to an object, then you call the relevant function to change a data field, and finally save the data. Code snippets are shown below:
val account = book.getRootAccount().getAccountByName("checking") // first obtain a reference to an Account object somehow
account.setEditingMode() // optional - do this when editing more than one field on this object
account.setAccountName("checking new")
account.setAccountDescription("new description")
account.syncItem() // this is the command to save your changes
Transactions specifically are modeled across three classes: AbstractTxn is the base class; ParentTxn is the transaction itself; SplitTxn is each individual line within the (parent) transaction. A ParentTxn holds its splits directly — use getSplitCount() / getSplit(i) to access splits. Always call syncItem() on the parent (not the splits) once you have finished all edits to a transaction.
Splits hold their own value in their own currency via SplitTxn.value (‘samt’ data key). The split’s effect on the parent, in the parent’s currency, is SplitTxn.parentAmount (or SplitTxn.getParentValue(), which is the same thing) (‘pamt’ data key). Don’t use SplitTxn.amount for this — despite a similar-sounding description, it returns the negative of parentAmount and will give you the wrong sign.
For example a split value 1.20 in GBP will be shown as parent amount GBP -1.20. If the parent currency was USD with a rate of 0.5 (where rate = split-currency units per 1 parent-currency unit — e.g. if GBP 1 = USD 2, the rate is 0.5), then the parent amount would be -2.40. On the ParentTxn object, ParentTxn#value is the sum of all its splits’ SplitTxn#parentAmount values. So for example, if there were two splits of GBP 10.00 and GBP 20.00 then the parent in GBP would show -30. Care should be taken with the various ways to set split and parent values, as these have a direct relationship and both the sign and rate are important.
val txn = obtainTxn // or create it
val parentTxn = txn.getParentTxn() // obtain the parent - will return itself if already a ParentTxn
parentTxn.setEditingMode() // allow multiple edits
manipulateTxns() // manipulate the parent and splits
parentTxn.syncItem() // save the parent txn - splits are part of the parent's record.
- Any
MoneydanceSyncableItemobject from which all Moneydance’s data objects extend (e.g.Account,ParentTxn,CurrencyType, etc.) can show you its own raw backing data — useful when exploring the data model. Callitem.syncInfo.toMultilineHumanReadableString()to get it as a string, ormdGUI.showRawItemDetails(item, parent)to pop up a read-only viewer window.
Listeners
Many listeners are available to allow your extension to be notified upon certain events. Key listeners are:
-
these can all be found within the core model
AccountListener,CurrencyListener,TransactionListener,PreferencesListener -
this code example demonstrates a template for implementing these listeners:
class MyClass(): AccountListener, CurrencyListener, PreferencesListener { fun demoAddRemoveListeners() { accountBook.addAccountListener(this); accountBook.removeAccountListener(this); accountBook.currencies.addCurrencyListener(this) accountBook.currencies.removeCurrencyListener(this); val prefs = context.preferences prefs.addListener(this); prefs.removeListener(this); } override fun currencyTableModified(table: CurrencyTable?) {} override fun accountModified(account: Account?) {} override fun accountBalanceChanged(account: Account?) {} override fun accountDeleted(parentAccount: Account?, deletedAccount: Account?) {} override fun accountAdded(parentAccount: Account?, newAccount: Account?) {} override fun preferencesUpdated() {} }always remove all your listeners when the dataset is closed (i.e. use
handleEvent()and monitormd:file:closingand md:file:closedsignals), and/or when your extension'sunload()andcleanup()` functions are called
It’s also possible to install an application event listener manually AppEventListener using context.addAppEventListener() and context.removeAppEventListener(). This allows you to receive events through handleEvent(). Extensions would not normally do this as FeatureModule classes already receive events through handleEvent(). But there are scenarios where you might find this useful.
Building GUI with Swing
Moneydance’s UI is built with Swing (javax.swing), and extensions use it too for any GUI they add — dialogs, home page widgets, config panels, etc. As with any Swing code, always create and update GUI components on the Event Dispatch Thread (EDT); use javax.swing.SwingUtilities.invokeLater(...) (or invokeAndWait if you need to block) when showing/updating UI from a background thread.
Rather than subclassing JDialog directly, extensions typically extend SecondaryDialog, which handles registration with Moneydance’s window management, size/location persistence, and standard close/escape-key behaviour for you:
class MyDialog(mdGUI: MoneydanceGUI, parent: java.awt.Component?): SecondaryDialog(mdGUI, com.moneydance.awt.AwtUtil.getFrame(parent), "My Extension", false) {
init {
setEscapeKeyCancels(true)
add(JLabel("Hello from my extension"))
pack()
setLocationRelativeTo(parent)
}
}
SwingUtilities.invokeLater { MyDialog(mdGUI, null).isVisible = true } // showing it, safely from any thread
Accessing files bundled inside the extension
FeatureModule provides getResourceAsStream(resourcePath) to read a file that’s been packaged inside your extension’s MXT. Available since Moneydance 2024.2 build 5142.
val text: String? = getResourceAsStream("/com/moneydance/modules/features/yourextnid/readme.txt")?.bufferedReader()?.use { it.readText() }
resourcePath is resolved via the extension’s own classloader, so it should match the path the resource was packaged at within the MXT — just like a standard Java classpath resource lookup. Returns null if the resource can’t be found, so always handle the null case.
Handling events
Extensions can receive notifications when certain events occur. To receive event notifications, override the following method:
override fun handleEvent(eventURI: String) {}
Events are identified by simple strings, and new ones may be added by newer versions of Moneydance. Some of the events that are fired include:
md:file:opening(file will be opened)md:file:opened(file has opened)md:file:closing(file will be closed)md:file:closed(file has closed)md:file:presave(dataset is about to flush data from memory to disk)md:file:postsave(dataset has flushed data from memory to disk)md:file:backupstarted(backup is about to start)md:file:backupfinished(backup has finished)md:account:root(summary / home page was selected)md:account:select(account was selected)md:app:onlinedownloadstarted(fiscal institution downloads have started)md:app:onlinedownloadfinished(fiscal institution downloads have finished)md:viewreminders(reminders were selected/viewed)md:viewbudget(budgets were selected/viewed)md:graphreport(graph/report was selected)md:licenseupdated(license was updated)md:app:exiting(application is closing)
MXT file structure
An MXT is a jar-format archive. Alongside your compiled classes and resources, two files at the root of your extension’s package are required:
com/moneydance/modules/features/yourextn/
├── Main.class ← FeatureModule entry point (by convention)
└── meta_info.dict ← metadata file that describes your extension
Main.class— the compiledFeatureModuleentry point (or whatever class name you register —Mainis just convention).meta_info.dict— extension metadata (module ID, name, version, etc.) that Moneydance reads to identify and load the extension.
Everything else (additional classes, icons, bundled resource files, util/ subpackages, etc.) is just packaged alongside these as normal.
Extension metadata (meta_info.dict)
meta_info.dict holds key/value metadata Moneydance uses to identify and describe the extension — vendor, vendor_url, module_name, module_desc, id, module_build, minbuild:
{
"vendor" = "Author's Name"
"vendor_url" = "https://github.com/TheInfiniteKind/moneydance_open"
"module_name" = "Extension Name"
"module_desc" = "Does cool things"
"id" = "myextension"
"module_build" = "1"
"minbuild" = "5253"
}
Coding and build tooling standards
- The DevKit’s build scripts use Gradle Wrapper 9.x (the bundled
gradlewcan be run using JDK 25) - Kotlin is the recommended language for new extensions. Java is equally supported if preferred, and Java and Kotlin can be mixed within the same extension.
- Whether written in Kotlin or Java, extensions run on Moneydance’s bundled JVM (MD2024: JRE21, MD2026: JRE25)
- The minimum JDK version is 17; JDK 25 is recommended. Java source, target, and release levels should be locked to 17.
- When using Kotlin, use at least the 1.9.x Kotlin plugin, with the language and API versions locked to 1.9.
- Kotlin plugin 2.3.21 is the latest release that supports Kotlin language/API version 1.9.
- Compile against the bundled kotlin-stdlib 1.9.x jar to match the Kotlin language/API version.
- When using this DevKit’s bundled Gradle build, the recommended Java and Kotlin versions are configured automatically.
- IntelliJ IDEA 2026 CE (free) is an excellent IDE and works with the bundled DevKit and these coding standards.
These recommendations provide the best chance of success, and backwards compatibility with previous Moneydance releases.
Appendix
Class reference and API documentation
Table of all Moneydance classes referenced across these developer guides, plus the standard Java/Swing classes used alongside them:
| Class | Full reference | apidoc |
|---|---|---|
AbstractTxn |
com.infinitekind.moneydance.model.AbstractTxn |
link |
Account |
com.infinitekind.moneydance.model.Account |
link |
Account.AccountType |
com.infinitekind.moneydance.model.Account.AccountType |
link |
AccountBook |
com.infinitekind.moneydance.model.AccountBook |
link |
AccountListener |
com.infinitekind.moneydance.model.AccountListener |
link |
AccountUtil |
com.infinitekind.moneydance.model.AccountUtil |
link |
AcctFilter |
com.infinitekind.moneydance.model.AcctFilter |
link |
ActionContextType |
com.moneydance.apps.md.controller.ActionContextType |
not in apidoc |
AppDebug |
com.infinitekind.util.AppDebug |
not in apidoc |
AppEventListener |
com.moneydance.apps.md.controller.AppEventListener |
not in apidoc |
AwtUtil |
com.moneydance.awt.AwtUtil |
link |
CollapsibleRefresher |
com.moneydance.awt.CollapsibleRefresher |
not in apidoc |
CurrencyListener |
com.infinitekind.moneydance.model.CurrencyListener |
link |
CurrencyTable |
com.infinitekind.moneydance.model.CurrencyTable |
link |
CurrencyType |
com.infinitekind.moneydance.model.CurrencyType |
link |
CurrencyType.Type |
com.infinitekind.moneydance.model.CurrencyType.Type |
link |
DateRange |
com.infinitekind.moneydance.model.DateRange |
link |
FeatureModule |
com.moneydance.apps.md.controller.FeatureModule |
link |
FeatureModuleContext |
com.moneydance.apps.md.controller.FeatureModuleContext |
link |
HomePageView |
com.moneydance.apps.md.view.HomePageView |
link |
MDAction |
com.moneydance.apps.md.view.gui.MDAction |
not in apidoc |
MDActionContext |
com.moneydance.apps.md.controller.MDActionContext |
not in apidoc |
MoneydanceGUI |
com.moneydance.apps.md.view.gui.MoneydanceGUI |
not in apidoc |
ParentTxn |
com.infinitekind.moneydance.model.ParentTxn |
link |
PreferencesListener |
com.moneydance.apps.md.controller.PreferencesListener |
not in apidoc |
SecondaryDialog |
com.moneydance.apps.md.view.gui.SecondaryDialog |
not in apidoc |
SplitTxn |
com.infinitekind.moneydance.model.SplitTxn |
link |
TransactionListener |
com.infinitekind.moneydance.model.TransactionListener |
link |
| Class | Full reference |
|---|---|
Action |
javax.swing.Action |
ActionEvent |
java.awt.event.ActionEvent |
Component |
java.awt.Component |
JComponent |
javax.swing.JComponent |
JDialog |
javax.swing.JDialog |
JFrame |
javax.swing.JFrame |
JLabel |
javax.swing.JLabel |
JPanel |
javax.swing.JPanel |
JTextField |
javax.swing.JTextField |
SwingUtilities |
javax.swing.SwingUtilities |
SwingWorker |
javax.swing.SwingWorker |
Infinite Kind’s Open source repository and reference implementations
- Open Source repository: https://github.com/TheInfiniteKind/moneydance_open
The Moneydance Open Source repository at https://github.com/TheInfiniteKind/moneydance_open and (GitHub repo) contains real, working extensions that you can clone, fork, and use as reference implementations or a starting point for your own extension. This open source repo is essentially a larger version of the DevKit:
git clone https://github.com/TheInfiniteKind/moneydance_open.git
There is also a wiki page which contains pointers to other developer’s work / pages: https://github.com/TheInfiniteKind/moneydance_open/wiki
It’s a good place to see complete, buildable examples of FeatureModule entry points, meta_info.dict layout, home page views, and context menu actions in context, rather than isolated snippets.