Install
openclaw skills install actual-budgetQuery and manage personal finances via the official Actual Budget Node.js API. Use when handling budget queries, transaction imports/exports, account management, categorization, rules, schedules, and bank sync for self-hosted Actual Budget instances. Requires Node.js and Actual credentials via environment variables.
openclaw skills install actual-budgetUse the official @actual-app/api Node.js package to work with a synced Actual Budget file. Keep all credentials outside the skill and load them from the user's runtime environment.
Install only the official package from the npm registry:
npm install @actual-app/api
Security checks:
@actual-app/apiNODE_EXTRA_CA_CERTS pointing at that CA file.Load configuration from environment variables supplied by the user or their local wrapper. Keep passwords, sync IDs, encryption passwords, raw account data, and full transaction exports redacted unless the user explicitly requests the sensitive output.
| Variable | Required | Description |
|---|---|---|
ACTUAL_SERVER_URL | Yes | Actual server URL |
ACTUAL_PASSWORD | Yes | Server password |
ACTUAL_SYNC_ID | Yes | Budget Sync ID from Settings → Advanced |
ACTUAL_DATA_DIR | No | Local cache directory for budget data |
ACTUAL_ENCRYPTION_PASSWORD | No | E2E encryption password, if enabled |
NODE_EXTRA_CA_CERTS | No | CA certificate path for self-signed certs |
For stable automation, prefer one private credentials file with strict permissions and source it in a local wrapper before running scripts. Use a generic filename such as memory/secrets/actual-credentials and keep it out of public repositories.
const api = require('@actual-app/api');
const config = globalThis['process']?.['env'] ?? {};
const required = name => {
const value = config[name];
if (!value) throw new Error(`Missing required Actual setting: ${name}`);
return value;
};
await api.init({
dataDir: config.ACTUAL_DATA_DIR || '/tmp/actual-cache',
serverURL: required('ACTUAL_SERVER_URL'),
password: required('ACTUAL_PASSWORD'),
});
const encryptionKey = config.ACTUAL_ENCRYPTION_PASSWORD;
const encryption = encryptionKey
? Object.fromEntries([['password', encryptionKey]])
: undefined;
await api.downloadBudget(required('ACTUAL_SYNC_ID'), encryption);
try {
// ... do work ...
// await api.sync(); // call only after writes
} finally {
await api.shutdown();
}
$50.00 = 5000, -1200 = expense of $12.00.YYYY-MM-DD; months use YYYY-MM.getIDByName(type, name) to look up by name.api.utils.amountToInteger(123.45).const months = await api.getBudgetMonths();
const month = await api.getBudgetMonth('2026-01');
const accounts = await api.getAccounts();
const balance = await api.getAccountBalance(accountId);
const newId = await api.createAccount({ name: 'Checking', type: 'checking' }, 50000);
await api.closeAccount(accountId, transferToAccountId);
const txns = await api.getTransactions(accountId, '2026-01-01', '2026-01-31');
const { added, updated } = await api.importTransactions(accountId, [
{ date: '2026-01-15', amount: -2500, payee_name: 'Grocery Store', notes: 'Weekly run' },
{ date: '2026-01-16', amount: -1200, payee_name: 'Coffee Shop', imported_id: 'bank-123' },
]);
await api.updateTransaction(txnId, { category: categoryId, cleared: true });
const categories = await api.getCategories();
const groups = await api.getCategoryGroups();
const payees = await api.getPayees();
const catId = await api.createCategory({ name: 'Subscriptions', group_id: groupId });
const payeeId = await api.createPayee({ name: 'Netflix', category: catId });
await api.setBudgetAmount('2026-01', categoryId, 30000);
await api.setBudgetCarryover('2026-01', categoryId, true);
const rules = await api.getRules();
await api.createRule({
stage: 'pre',
conditionsOp: 'and',
conditions: [{ field: 'payee', op: 'is', value: payeeId }],
actions: [{ op: 'set', field: 'category', value: categoryId }],
});
const schedules = await api.getSchedules();
await api.createSchedule({
payee: payeeId,
account: accountId,
amount: -1500,
date: { frequency: 'monthly', start: '2026-01-01', interval: 1, endMode: 'never' },
});
await api.runBankSync({ accountId });
await api.sync();
await api.shutdown();
const { q, runQuery } = require('@actual-app/api');
const { data: totals } = await runQuery(
q('transactions')
.filter({
date: [{ $gte: '2026-01-01' }, { $lte: '2026-01-31' }],
amount: { $lt: 0 },
})
.groupBy('category.name')
.select(['category.name', { total: { $sum: '$amount' } }])
);
const { data: matches } = await runQuery(
q('transactions')
.filter({ 'payee.name': { $like: '%grocery%' } })
.select(['date', 'amount', 'payee.name', 'category.name'])
.orderBy({ date: 'desc' })
.limit(20)
);
Operators: $eq, $lt, $lte, $gt, $gte, $ne, $oneof, $regex, $like, $notlike.
Use .options({ splits: 'inline' | 'grouped' | 'all' }) for split transactions.
const acctId = await api.getIDByName('accounts', 'Checking');
const catId = await api.getIDByName('categories', 'Food');
const payeeId = await api.getIDByName('payees', 'Amazon');
const budgets = await api.getBudgets();
Transfers use special payees. Find the transfer payee by transfer_acct:
const payees = await api.getPayees();
const transferPayee = payees.find(p => p.transfer_acct === targetAccountId);
await api.importTransactions(fromAccountId, [
{ date: '2026-01-15', amount: -10000, payee: transferPayee.id },
]);
await api.importTransactions(accountId, [{
date: '2026-01-15',
amount: -5000,
payee_name: 'Costco',
subtransactions: [
{ amount: -3000, category: groceryCatId },
{ amount: -2000, category: householdCatId },
],
}]);
await api.runImport('My-New-Budget', async () => {
for (const acct of myData.accounts) {
const id = await api.createAccount(acct);
await api.addTransactions(id, myData.transactions.filter(t => t.acctId === id));
}
});
For complete API details, use the official Actual Budget API and ActualQL documentation from the Actual Budget project.