Post cashflows to accounts
Use account() to define balances, then use bookings, debit, or credit to post each cashflow occurrence. Each account carries its own type and reporting category. This lets a model book to a person-selected bank account without requiring a built-in person or account name in the engine.
import { account, cashflow, category, currencies, defineModel, entity, periods, run, schedule, trigger } from "deccf";
const anna = entity.party();
const bank = account({ owner: anna, type: "asset", category: category("anna.assets.cash.bank") });
const salaryIncome = account({ owner: anna, type: "income", category: category("anna.income.salary") });
const salary = cashflow({ debit: bank, credit: salaryIncome, schedule: schedule.monthly(), amount: 52_000 });
const model = defineModel({
name: "anna-salary",
currency: currencies.SEK,
timeline: periods.monthly({ from: "2026-01", count: 12 }),
entities: { anna },
accounts: { bank, salaryIncome },
flows: { salary },
});
return model;A simple payment
import { account, cashflow, category, currencies, defineModel, entity, periods, run, schedule } from "deccf";
const anna = entity.party();
const bankCategory = category("anna.assets.cash.bank");
const rentIncomeCategory = category("anna.income.rent");
const bank = account({ owner: anna, type: "asset", category: bankCategory });
const rentIncome = account({ owner: anna, type: "income", category: rentIncomeCategory });
const rent = cashflow({
debit: bank,
credit: rentIncome,
schedule: schedule.monthly(),
amount: 12_000,
});
const model = defineModel({
name: "Anna rents out a room",
currency: currencies.SEK,
timeline: periods.monthly({ from: "2026-01", count: 12 }),
entities: { anna },
accounts: { bank, rentIncome },
flows: { rent },
});
const result = run(model);
return model;The debit / credit shorthand posts the same base amount to both accounts. This occurrence increases the bank balance by 12,000 and increases the income account by a credit of 12,000. Debits are positive account movements and credits are negative. In income reports, the engine uses economic signs so income is shown as positive and expense as negative.
The flow amount is 12,000 from the bank's debit movement. Since the bank is the only posted asset account, the engine identifies it as the cash account. This posting cashflow does not need owner, direction, or an explicit cashAccount. Because both accounts have the same owner, the flow also inherits Anna as its owner. Set a category on each account whose postings should be filtered or summarized.
import { account, cashflow, category, currencies, defineModel, entity, periods, run, schedule } from "deccf";
const anna = entity.party();
const bankCategory = category("anna.assets.cash.bank");
const rentIncomeCategory = category("anna.income.rent");
const bank = account({ owner: anna, type: "asset", category: bankCategory });
const rentIncome = account({ owner: anna, type: "income", category: rentIncomeCategory });
const rent = cashflow({ debit: bank, credit: rentIncome, schedule: schedule.monthly(), amount: 12_000 });
const model = defineModel({
name: "anna-rent-income",
currency: currencies.SEK,
timeline: periods.monthly({ from: "2026-01", count: 12 }),
entities: { anna },
accounts: { bank, rentIncome },
flows: { rent },
});
const result = run(model);
result.account(bank); // closing bank balance by period
result.category(bankCategory); // bank posting movements
result.category(rentIncomeCategory); // income with positive sign
return model;The category of a posting row comes from the account targeted by that row. A debit to an expense account and credit to a bank account therefore use two different categories. Do not also set category on a posting cashflow; set categories on the accounts.
Calculate an amount from another cashflow
The amount function works the same way for posting cashflows and basic signed cashflows. Add a source to dependsOn when reading it in the same period:
import { account, cashflow, category, currencies, defineModel, entity, periods, schedule } from "deccf";
const anna = entity.party();
const bank = account({ owner: anna, type: "asset", category: category("anna.assets.cash.bank") });
const rentIncome = account({ owner: anna, type: "income", category: category("anna.income.rent") });
const rent = cashflow({ debit: bank, credit: rentIncome, schedule: schedule.monthly(), amount: 12_000 });
const adjustmentIncome = account({
owner: anna,
type: "income",
category: category("anna.income.rent.adjustment"),
});
const rentAdjustment = cashflow({
debit: bank,
credit: adjustmentIncome,
schedule: schedule.monthly(),
dependsOn: [rent],
amount: (ctx) => ctx.get(rent).amount * 0.05,
});
const model = defineModel({
name: "rent-with-adjustment",
currency: currencies.SEK,
timeline: periods.monthly({ from: "2026-01", count: 12 }),
entities: { anna },
accounts: { bank, rentIncome, adjustmentIncome },
flows: { rent, rentAdjustment },
});
return model;Add adjustmentIncome to the model's accounts map and rentAdjustment to its flows map. ctx.get(rent) returns the already-calculated signed Money amount for the current period.
Split one transaction
Use bookings when a transaction touches more than two accounts. Each row identifies exactly one debit or credit account. The amount can be a number, money value, input reference, or synchronous function.
import { account, cashflow, category, currencies, defineModel, entity, periods, schedule } from "deccf";
const anna = entity.party();
const bank = account({ owner: anna, type: "asset", category: category("anna.assets.cash.bank") });
const rentIncome = account({ owner: anna, type: "income", category: category("anna.income.rent") });
const receivables = account({
owner: anna,
type: "asset",
category: category("anna.assets.receivables.rent"),
});
const billedRent = cashflow({
cashAccount: bank,
schedule: schedule.monthly(),
amount: 12_000,
bookings: [
{ debit: bank, amount: (ctx) => ctx.amount / 2 },
{ debit: receivables, amount: (ctx) => ctx.amount / 2 },
{ credit: rentIncome, amount: (ctx) => ctx.amount },
],
});
const model = defineModel({
name: "rent-billing-split",
currency: currencies.SEK,
timeline: periods.monthly({ from: "2026-01", count: 12 }),
entities: { anna },
accounts: { bank, receivables, rentIncome },
flows: { billedRent },
});
return model;ctx.amount is the cashflow's base amount. Half is booked directly to the bank and half to receivables; the full amount is credited to the income account. Debits and credits must balance on every occurrence or the run stops with DECCF_UNBALANCED_BOOKING. Booking amounts must be finite and non-negative. Reverse a booking by switching its side.
The flow's cash movement is determined by cashAccount. Here it is 6,000 per month, while rent income is 12,000. An explicit cashAccount is needed because both bank and receivables are assets. A posting cashflow still does not need direction.
Account and category signs
| Posting row | Signed account movement | Category flow |
|---|---|---|
Debit to asset or liability | Positive | Positive |
Credit to asset or liability | Negative | Negative |
Debit to income or expense | Positive | Negative |
Credit to income | Negative | Positive |
Credit to expense | Negative | Positive |
type only changes how income and expense category movements are reported. It does not change the account balance rule. result.category(ref) includes descendant categories. result.account(ref) returns ending balances, not period postings. If several accounts share a category, their posting movements are summed there.
Import a historical statement
statement() creates a flow group that brings account balances to absolute target values. It reads the current balances, calculates the difference, and posts balanced debit and credit rows. A one-time schedule applies the targets at one point. A recurring schedule spreads the remaining difference evenly across its remaining occurrences, reaching each target on the final scheduled occurrence. Use the same calendar schedules as for other flows, such as monthly, quarterly, or annual schedules. Accounts created with stdaccount() are collected automatically; ordinary account() references still need to be declared in accounts.
Statement values use report signs: income is positive, expenses are negative, and ordinary asset, liability, and equity values are positive. The engine converts those values to its signed debit-minus-credit account balances. Untyped accounts use signed ledger values directly. If the adjustments do not balance, provide balancingAccount to post the opposite net movement to an explicit account, or include the missing account values in the statement.
This example imports an annual income statement evenly over its monthly periods. The clearing account makes the imported postings balance; it is an ordinary account selected by the model author.
import { account, category, currencies, defineModel, entity, periods, schedule, statement } from "deccf";
const company = entity.asset();
const revenue = account({ owner: company, type: "income", category: category("company.income.sales") });
const operatingCosts = account({ owner: company, type: "expense", category: category("company.expense.operating") });
const importClearing = account({ owner: company, type: "asset", category: category("company.assets.import_clearing") });
const historicalIncomeStatement = statement({
name: "historical-2025-income-statement",
owner: company,
schedule: schedule.monthly({ from: "2025-01", to: "2025-12" }),
balances: [
{ account: revenue, value: 1_200_000 },
{ account: operatingCosts, value: -780_000 },
],
balancingAccount: importClearing,
});
const model = defineModel({
name: "historical-income-statement-import",
currency: currencies.SEK,
timeline: periods.monthly({ from: "2025-01", count: 12 }),
entities: { company },
accounts: { revenue, operatingCosts, importClearing },
flows: { historicalIncomeStatement },
});
return model;For a balance sheet, give all included balance accounts their absolute values at the reporting date. The entries must form a balanced set unless you choose a balancing account. This complete year-end example has 120,000 in assets and 120,000 in liabilities and equity:
import { account, category, currencies, defineModel, entity, periods, schedule, statement } from "deccf";
const company = entity.asset();
const bank = account({ owner: company, type: "asset", category: category("company.assets.cash") });
const supplierDebt = account({ owner: company, type: "liability", category: category("company.liabilities.suppliers") });
const shareCapital = account({ owner: company, type: "equity", category: category("company.equity.share_capital") });
const retainedEarnings = account({ owner: company, type: "equity", category: category("company.equity.retained_earnings") });
const yearEndBalanceSheet = statement({
name: "2025-year-end-balance-sheet",
owner: company,
schedule: schedule.once("2025-12-31", { placement: "end" }),
balances: [
{ account: bank, value: 120_000 },
{ account: supplierDebt, value: 20_000 },
{ account: shareCapital, value: 70_000 },
{ account: retainedEarnings, value: 30_000 },
],
});
const model = defineModel({
name: "year-end-balance-sheet-import",
currency: currencies.SEK,
timeline: periods.monthly({ from: "2025-01", count: 12 }),
entities: { company },
accounts: { bank, supplierDebt, shareCapital, retainedEarnings },
flows: { yearEndBalanceSheet },
});
return model;The same API can set one account or many accounts. A quarterly or annual schedule spreads each remaining adjustment evenly over its scheduled occurrences and reaches the target at the final occurrence.
Close income and expense accounts with an account plan
The account plan's bookClosing() creates an ordinary posting cashflow. Provide the owner and schedule. The closing operation finds the owner's income and expense accounts from their type and finds retained earnings from the category recorded by the plan. At each scheduled occurrence, it zeros the income and expense accounts and transfers the net amount to retained earnings.
import { cashflow, currencies, defineModel, entity, periods, schedule } from "deccf";
import { businessAccountPlan, stdaccount } from "deccf/domainpacks/accountplan";
const company = entity.asset();
const bank = stdaccount({ code: "1930", owner: company, initial: 100_000 });
const shareCapital = stdaccount({ code: "2081", owner: company, initial: -100_000 });
const revenueAccount = stdaccount({ code: "3000", owner: company });
const taxExpenseAccount = stdaccount({ code: "8910", owner: company });
const revenue = cashflow({ owner: company, debit: bank, credit: revenueAccount, schedule: schedule.monthly(), amount: 20_000 });
const taxPayment = cashflow({
owner: company,
dependsOn: [revenue],
debit: taxExpenseAccount,
credit: bank,
schedule: schedule.monthly(),
amount: (ctx) => ctx.get(revenue).amount * 0.2,
});
const taxExpense = taxPayment;
const yearEnd = businessAccountPlan.bookClosing({
owner: company,
schedule: schedule.annual({ from: "2027-12", to: "2030-12" }),
dependsOn: [taxExpense],
});
const model = defineModel({
name: "business-book-closing",
currency: currencies.SEK,
timeline: periods.monthly({ from: "2026-01", count: 60 }),
entities: { company },
accounts: { shareCapital },
flows: { revenue, taxPayment, yearEnd },
});
return model;Open a business forecast with account-plan accounts in the Calculator (available in Marketplace).
The bank, revenue, and tax expense accounts are collected from their flows; the bank keeps its opening value from its stdaccount declaration. Book closing obtains retained earnings through the plan. Share capital stays in accounts because no flow otherwise refers to it.
All income and expense accounts owned by company are closed. The business plan also accepts dividend and contribution as amounts or functions. A dividend debits retained earnings and credits a cash account; a contribution debits cash and credits the plan's shareholder contribution account. The business plan normally selects account 1930; set cashAccount to choose another cash account. The personal plan does not accept dividends or contributions.
The model author controls ordering. Flows follow the model's declared order, and dependsOn can add explicit dependencies. In this example, the closing operation waits for taxExpense; the account-plan module does not know the tax flow's role.
Delayed and recurring postings
A trigger sends an immutable snapshot from each source occurrence to a listening cashflow. The delay uses whole days, but the receiving flow runs on a period representable by the model timeline.
import { account, cashflow, category, currencies, defineModel, entity, periods, schedule, trigger } from "deccf";
const anna = entity.party();
const bank = account({ owner: anna, type: "asset", category: category("assets.cash") });
const receivables = account({ owner: anna, type: "asset", category: category("assets.receivables") });
const rentIncome = account({ owner: anna, type: "income", category: category("income.rent") });
const latePay = trigger("latePay");
const billedRentWithTrigger = cashflow({
cashAccount: bank,
schedule: schedule.monthly(),
amount: 12_000,
bookings: [
{ debit: bank, amount: (ctx) => ctx.amount / 2 },
{ debit: receivables, amount: (ctx) => ctx.amount / 2 },
{ credit: rentIncome, amount: (ctx) => ctx.amount },
],
triggers: latePay,
});
const delayedSettlement = cashflow({
cashAccount: bank,
schedule: schedule.onceAfterNoOfDays(30),
triggeredBy: latePay,
bookings: [
{ debit: bank, amount: (ctx) => ctx.source.amount / 2 },
{ credit: receivables, amount: (ctx) => ctx.source.amount / 2 },
],
});
const model = defineModel({
name: "delayed-rent-settlement",
currency: currencies.SEK,
timeline: periods.monthly({ from: "2026-01", count: 12 }),
entities: { anna },
accounts: { bank, receivables, rentIncome },
flows: { billedRentWithTrigger, delayedSettlement },
});
return model;triggers fires for every scheduled occurrence of billedRentWithTrigger. The delayed cashflow runs once per received trigger, and ctx.source.amount preserves the amount from that exact source occurrence. Multiple triggers due in the same period create multiple occurrences. Set triggeredBy when the model emits more than one trigger type; when there is only one type, the engine can infer it. Both posting cashflows inherit Anna as their owner because all posted accounts have that owner. direction can also be omitted.
The delay starts from the source's scheduled placement (start, mid, or end). The due date maps to the model timeline and cannot execute in the same period as the source. A monthly timeline cannot guarantee the exact day 30 days later; use a daily timeline when that precision is required.
See the time and schedules reference and calculation context reference.