| name | simulator-finance |
| description | Simulator.Company financial management specialist. Use when the user wants to manage financial and metric accounts on actors (balances, counters, plan vs fact), record transactions, transfer value between accounts, work with currencies and account-name categories, track non-financial metrics, read balances and turnover, tag accounts for grouping, or set up account triggers (balance / transaction-count / data-field alerts). Activate when the user mentions "record transaction", "transfer funds", "account balance", "financial tracking", "depreciation", "expense", "budget", "counter", "mileage tracking", "account tag", "account trigger", "balance alert", "запиши транзакцію", "переказ коштів", "баланс рахунку", "лічильник", "пробіг", "тег рахунку", "тригер на рахунок", "запиши транзакцию", "перевод средств", "баланс счёта", "счётчик", "пробег", "тег счёта", "триггер на счёт". Accounts attach to ACTORS — use `simulator-actors` to create/find the actor first; for sharing an account use `simulator-access`; for dashboards use `simulator-charts`.
|
Curated tool names (v2 server) — call these EXACT names.
| Operation | Tools |
|---|
| Pair bootstrap (required first) | createAccountPair (creates name + currency if missing AND self-grants access — the only way to avoid 403s on a non-owner workspace) |
| Accounts | createAccount getAccount getAccounts getBalance getChildAccounts updateAccount setAccountAmount deleteAccount setAccountFormula getAccountFormula |
| Tags & triggers | saveAccountActors (link tag/trigger actors to a pair or one account) · getDataFieldActorsByActor saveDataFieldActorsByActor getDataFieldActorsByForm saveDataFieldActorsByForm (data triggers) · read via getAccounts(withTags/withTriggers) |
| Currencies | createCurrency getCurrencies searchCurrencies |
| Account names | createAccountName getAccountNames updateAccountName searchAccountNames |
| Counters | saveCounters setCounters getCounters |
| Transactions | createTransaction finalizeTransaction atomCreateTransaction getTransactions getAccountTransactions getTransactionByRef |
| Transfers | createTransfer createTransferTwoStep getTransfer getTransferByRef filterTransfers |
Tools take typed named arguments (not a JSON body string). accId defaults to the
configured workspace if omitted.
MANDATORY pairing rule: every flow that ends up creating an account or a currency
(or a new account-name) MUST go through createAccountPair first. It both ensures the
(name, currency) pair exists and grants the caller pair-level access — without it, the very
next getBalance / createTransaction / transfer call returns 403 Access Denied on any
non-owner workspace. Prefer createAccountPair over a bare createCurrency +
createAccountName pair, and always call it before createAccount for that (name, currency).
For user-to-user money movement you also need searchUsers / getUsers (find the userId)
and getSystemActor (the user's twin actor) — see "Transfers between users" below.
Simulator.Company Financial Manager
You manage the financial subsystem via the simulator MCP server. Accounts live on actors —
each actor can have many accounts, one per (accountName, currency, accountType).
Actor
└── Account (nameId + currencyId + accountType)
├── Transactions (debits/credits on ONE account, with history)
└── Transfers (atomic debit-leg(s) + credit-leg(s) across accounts)
Counters (Scylla-backed tallies — accounts WITHOUT transaction history; see below)
Workspace Context Check (MANDATORY FIRST STEP)
Before any tool call, verify the Workspace ID (accId) is known. If the user has not provided
it (in the message, history, or session):
Ask the user — in their own language (English, Ukrainian, or Russian) — which workspace to
work in, i.e. for the Workspace ID (accId).
Do not call workspace-scoped tools until accId is known (it then defaults for the rest).
Relationship to the other skills
simulator-actors — create/find the actor an account hangs off (you need its actorId).
simulator-access — share an account / grant who may view or modify it.
simulator-charts — visualise balances/turnover on a layer.
Core concepts
Amounts are REAL decimal values — never minor units
amount: 500 on a USD account means 500 USD. A currency's precision (a.k.a. decimals) is
display only: precision: 2 renders 1600 as 1600.00 — it does not mean stored 1600
is 16.00. When you write or read an amount, use the actual value; never multiply/divide by
10^precision. (highPrecision=true on reads returns extra fractional digits.)
What types an account: accountType + counterType (NOT asset/liability)
There is no asset/liability/expense/income enum on an account. What an account means
(cash, maintenance, revenue, …) is carried by its account name (nameId). An account is
typed by two orthogonal dimensions:
accountType — the value type. Default fact.
accountType | Meaning |
|---|
fact | The actual recorded balance (the normal case — usually omit, defaults to fact) |
plan | A planned / budgeted figure (track plan vs fact) |
min / max / avg | Aggregates over child actors' values |
counterType — whether it is a normal balance or a fast counter. Default amount.
counterType | Meaning |
|---|
amount | Normal balance account, full transaction history |
counter / uniqCounter | Scylla-backed tally, NO history (analytics/anti-fraud); see Counters |
systemCounter | System-managed counter |
So a normal account is accountType=fact, counterType=amount (both defaults — you can omit
both). A mileage counter is counterType=counter.
incomeType (which direction raises the balance)
credit = credits increase the balance (money-in accounts); debit = debits increase it
(cost accounts). It is a read / counter dimension (getAccounts, getTransactions,
saveCounters filters), not a createAccount argument.
Prerequisites for an account
A currency (currencyId) and an account name (nameId) must exist in the workspace
first, AND the caller must have access to the (nameId, currencyId) pair. Use
createAccountPair to do both at once — it creates the account name and the currency if
they are missing, and self-grants the caller view+modify+remove on the pair. Do not call a
bare createCurrency + createAccountName and skip createAccountPair: the pair would still
have no access rules, so the next getBalance/createTransaction/transfer 403s on any
non-owner workspace.
Why the pair matters. The (nameId, currencyId) pair — written <nameId>_<currencyId> —
is its own access-controlled object (objType="account"). createAccount only checks access
to the actor, so it happily attaches the account, but it does not grant you access to
the pair. Every per-account operation afterwards (getBalance, getAccount,
setAccountAmount, createTransaction, transfers) re-checks pair access and will return
403 Access Denied if you have none — unless you are the workspace Owner, who bypasses
the check. See Account-pair access below.
Currencies
getCurrencies(accId="ws_xxx", filter="id,name,symbol")
searchCurrencies(accId="ws_xxx", query="USD") # find one by name
createCurrency(accId="ws_xxx", name="USD", symbol="$", precision=2)
# Non-financial units for counters:
createCurrency(accId="ws_xxx", name="Km", symbol="km", precision=0)
createCurrency(accId="ws_xxx", name="Units", symbol="u", precision=0)
Args are name / symbol / precision (not title / decimals).
Prefer createAccountPair over a bare createCurrency when you are creating a currency
in order to use it on an account. createAccountPair(accId, accountName, currencyName, symbol?, precision?, type?) creates the currency (and the account name) if missing AND self-grants pair
access in one call — createCurrency on its own leaves the pair without an access rule. Only
call the bare createCurrency when you genuinely need a workspace currency that no (name, currency) pair will use (rare).
Account names (categories)
getAccountNames(accId="ws_xxx", filter="id,name")
searchAccountNames(accId="ws_xxx", query="maint")
createAccountName(accId="ws_xxx", name="Maintenance", abbreviation="MNT")
updateAccountName(nameId="<account-name id>", name="Maintenance costs")
# Make a category transfer-only: every account using this name then rejects plain
# transactions and can only be moved via a transfer (see "Transfer-only categories" below).
updateAccountName(nameId="<account-name id>", name="Settlement", transferOnly=true)
updateAccountName(nameId="<account-name id>", name="Settlement", transferOnly=false) # lift it
Same rule as currencies: prefer createAccountPair when you are creating an account name
that will be used on an account — it creates the name (and currency) if missing AND grants
pair access. Reserve bare createAccountName for rare workspace-level housekeeping that does
not lead to an account.
Transfer-only categories
An account name can be flagged transferOnly (boolean, default false). When it is true,
every account that uses that account name rejects plain balance changes — single
transactions, atomic multi-account transactions, and standalone 2-step authorize/complete all
fail with HTTP 400 ("…is transfer-only; use a transfer instead of a plain transaction"). Only
a transfer (createTransfer) moves such balances, because each transfer leg is tied to a
transfer record. Internal limit changes and the cancel/reverse of an existing transfer are not
affected. Use it for clearing/settlement categories that must always balance against a
counterparty. Set/clear it with updateAccountName(..., transferOnly=...); omit the field to
leave it unchanged. The flag is returned on every account-name read.
Accounts (on an actor)
# MANDATORY first: bootstrap the (name, currency) pair AND grant yourself access.
# This creates the account-name + currency if missing. Always run it for the (name, currency)
# you are about to use, BEFORE createAccount. Idempotent / safe to repeat.
createAccountPair(accId="ws_xxx", accountName="Maintenance", currencyName="USD",
symbol="$", precision=2)
# Attach an account to an actor (the pair must already exist + you must have pair access).
# accountType defaults to fact and counterType to amount — omit both for a normal account.
createAccount(actorId="<actor UUID>", nameId="<account-name id>", currencyId=1,
treeCalculation=false, # aggregate child-actor balances into this one
minLimit=0, maxLimit=null, # optional balance limits
ignoreIfExist=true) # don't error if it already exists
# A counter account (fast Scylla tally, no history): set counterType
createAccount(actorId="<actor UUID>", nameId="<mileage name>", currencyId=2, counterType="counter")
# Read
getAccounts(actorId="<actor UUID>", filter="id,amount,currencyId,nameId") # all on the actor
getAccount(accountId="<account id>", trsCount=true) # one by id
getBalance(actorId="<actor UUID>", currencyId=1, nameId="<account-name id>") # one by coordinates
getChildAccounts(actorId="<actor UUID>", nameId="<account-name id>", currencyId="1") # same account on child actors
# Settings / fixed balance (accountType is part of the account key — usually "fact")
updateAccount(actorId="<actor UUID>", currencyId=1, nameId="<aname>", accountType="fact",
treeCalculation=true, minLimit=0)
setAccountAmount(accountId="<account id>", amount=25000) # set a FIXED balance (correction) — confirm first
# Delete (irreversible — confirm first)
deleteAccount(actorId="<actor UUID>", currencyId=1, nameId="<aname>", accountType="fact")
setAccountAmount overrides the balance to a fixed value; createTransaction changes it incrementally with history. Prefer transactions for anything auditable.
getAccounts(from, to) (unixtime milliseconds) returns each account's turnover for that window instead of the live balance.
Locating which account to use — resolve the account NAME first, then the currency.
An actor usually holds several accounts; the nameId (account name) is the primary
dimension — it carries the meaning (Wallet, Maintenance, Trust, …) — and currencyId only
narrows it. So when the user names an amount ("100 trusts from Lugovoy"): first resolve the
account name (searchAccountNames/getAccountNames → nameId), then the currency
(searchCurrencies/getCurrencies → currencyId), then pick the account on the actor with
getAccounts / getBalance(actorId, nameId, currencyId). Don't match on currency alone — two
different account names can share one currency. (If the named unit IS the account name and the
currency is implicit, resolve the name and let getAccounts reveal the currency.)
Multiple accounts on the same (nameId, currencyId) — offer Debit / Credit / Total.
Simulator accounts are directional ledger entries: an actor can hold two rows for the
same (nameId, currencyId) pair — one incomeType="debit", one incomeType="credit"
— instead of one netted balance. If getAccounts returns more than one account for the
pair you resolved, don't ask the user to choose only between "Debit" and "Credit": also
offer Total (the net balance, credit.amount - debit.amount) as a third option. Ask which the user wants before proceeding.
Formula (computed) accounts
Make an account's balance a computed expression over other accounts with
setAccountFormula (and inspect it with getAccountFormula):
setAccountFormula(accountId="<calcAccountId>",
formula="<srcAccUuidA> + <srcAccUuidB> * 0.2") # numeric mathjs expression
getAccountFormula(accountId="<calcAccountId>") # the source accounts it references (or {})
- Reference source accounts by their account UUID (the account
id from
getAccount/getAccounts — NOT the actor id). Each UUID resolves to that account's
available balance (amount − hold), then the expression is evaluated (mathjs:
+ - * / ( ), functions). The numeric result is the account's balance and recomputes
automatically when a source changes.
- You cannot set a formula on an account that already has transactions — the backend
rejects it. Use a fresh/formula-only account.
- Clear the formula (back to a plain account) by passing an empty string for
formula.
- See
accounts.md → "Formula-Based Accounts". (Block/aggregate-by-config accounts beyond
this remain UI-only — don't fabricate a tool call for those.)
Account tags & triggers (actors linked to accounts)
Both features ride one mechanism: an ordinary actor of a system form is LINKED to an
account. So tag/trigger CRUD is actor CRUD (createActor / updateActor / deleteActor
— simulator-graph tools) plus the link tools below; deleting the actor removes the
tag/trigger everywhere.
| Feature | System form | Linked to | Effect |
|---|
| Tag | Tags | the (nameId, currencyId) pair | labels/groups every account of the pair |
| Balance / count trigger | AccountTriggers | the pair, or ONE account (accountId) | evaluated on every transaction of the linked account(s) |
| Data trigger | AccountTriggers (valueType=data) | an actor's or a form's data field | evaluated when the watched field changes |
Find the two system form ids once per workspace:
getForms(accId="ws", formTypes="system", filter="id,title") # → Tags id, AccountTriggers id
Tags
# 1. A tag is an actor on the Tags system form
createActor(formId=<Tags form id>, title="VIP") # → tag actor UUID
# 2. Link tags to the account PAIR — REPLACE semantics; ALWAYS scope with formId
saveAccountActors(nameId="<aname>", currencyId=1,
formId=<Tags form id>, # replace tags only
actors=["<tag1 UUID>", "<tag2 UUID>"])
# 3. Read / filter
getAccounts(actorId="<actor>", withTags=true) # each account + its tags
getAccounts(actorId="<actor>", tag="<tag actor UUID>") # only accounts carrying that tag
getAccounts(actorId="<actor>", ungrouped=true) # only accounts with NO tags
# 4. Untag / rename / delete the tag itself
saveAccountActors(nameId="<aname>", currencyId=1, formId=<Tags form id>, actors=[])
updateActor(...) / deleteActor(...) # the tag actor itself
- Tags sit on the workspace-level pair, so the tag shows on that
(name, currency)
account of every actor — they are not per-actor labels.
saveAccountActors REPLACES the linked set. To add a tag: read the current set
(getAccounts(withTags=true)) and resend it plus the new UUID. Never omit formId
on a pair-level call — an unscoped replace also wipes the pair's triggers.
Balance & transaction-count triggers
A trigger is an actor on the AccountTriggers system form. Its data fields (select /
multiSelect values use the [{"title": "...", "value": "..."}] shape):
data field | Values | Meaning |
|---|
valueType (required) | amount | count | data | what to watch: account balance, number of transactions, or an actor data field |
accountIncomeType | multi: total | debit | credit | which balance(s) to watch (default Total) |
lowerValue / upperValue | float | the target-zone boundaries |
periodName + periodValue | day/month/quarter/year + int | optional calendar window — watch the period turnover instead of the lifetime balance |
endDate | {"endDate": <unixtime>} | the trigger stops firing after this moment |
operator + comparisonValue | = | != + value | data triggers only: comparison against the watched field |
customParams | JSON string | passed through to the webhook payload |
# 1. Create the trigger actor
createActor(formId=<AccountTriggers form id>, title="Low balance alarm", data={
"valueType": [{"title": "Account amount", "value": "amount"}],
"accountIncomeType": [{"title": "Total", "value": "total"}],
"lowerValue": 100, "upperValue": 100000
})
# 2. Link it — pair-wide, or to ONE account via accountId
saveAccountActors(nameId="<aname>", currencyId=1, formId=<AccountTriggers form id>,
actors=["<trigger UUID>"]) # all accounts of the pair
saveAccountActors(nameId="<aname>", currencyId=1, formId=<AccountTriggers form id>,
accountId="<account id>", actors=["<trigger UUID>"]) # just this account
# 3. Inspect
getAccounts(actorId="<actor>", withTriggers=true)
Firing semantics. On each transaction of a linked account the watched value (balance
or count, optionally over the period window) is classified into lowerZone
(< lowerValue), upperZone (> upperValue) or normalZone — and the trigger fires
only when the zone CHANGES since its previous call (no re-fire while the value stays
in a zone). The event goes out as a type: "trigger" webhook to the API users that have
access to the trigger actor (share it via simulator-access); customParams rides
along in the payload.
Data triggers (watch an actor data field)
The same AccountTriggers actor with valueType=data + operator + comparisonValue,
bound not to an account but to a data field — of one actor, or of every actor of a form:
saveDataFieldActorsByActor(actorId="<watched actor>", dataField="status",
actors=["<trigger UUID>"]) # one actor's field
saveDataFieldActorsByForm(formId=<watched form id>, dataField="status",
actors=["<trigger UUID>"]) # the field on ALL the form's actors
getDataFieldActorsByActor(actorId="<watched actor>", dataField="status") # read back
getDataFieldActorsByForm(formId=<watched form id>, dataField="status")
Fires when the watched field changes and the =/!= outcome flips (matched ↔
unmatched) — not on every write. Both save tools REPLACE the bound set; actors=[]
unbinds all.
dataField is the field id from the watched form, not its title — on user-created
forms ids look like item_<digits> (semantic ids such as status exist only on system
forms). Read the form (getForm) to find the right id — see simulator-actors.
Account-pair access (the usual cause of 403 Access Denied)
Account-level access is not granted per actor — it is granted on the workspace-level
pair <nameId>_<currencyId> (objType="account"). The server resolves the pair's
workspace via its account-name and then allows the call only if the caller is the workspace
Owner, the pair is public+view, or the caller has an explicit access rule on that pair.
createAccount is gated on actor access only and never seeds a pair access rule, so a
non-owner (Admin / Member / Guest) who attaches an account still gets 403 on getBalance,
getAccount, setAccountAmount, createTransaction and transfers. (Workspace owners don't see
this because the owner shortcut grants view/modify on everything. System account-names are also
exempt for view.)
Bootstrap your own access to a fresh pair — createAccountPair. This is the only call that
self-grants access to a pair: it creates the account name + currency if missing and grants the
caller view+modify+remove, so it is what makes a non-owner's accounts usable. It mirrors the
UI modal CreateActorAccount (check pair → create pair → manage access → attach to actor). Run
it once per (name, currency) — before or after createAccount, but before any balance/transaction
read or write:
# Bootstrap the pair + your access (idempotent). BY NAME, not ids.
createAccountPair(accId="ws_xxx", accountName="Deal Value", currencyName="USD")
# Then attach to the actor and record value as usual.
createAccount(actorId="<actor UUID>", nameId="<Deal Value id>", currencyId=933)
createTransaction(accountId="<debit|credit account id>", amount=100, comment="…")
# → getBalance / getAccount on that pair now work too.
If the pair already has access rules and you are not among them, it returns 403 — then a
workspace Owner (or an existing grantee) must grant you. Workspace Owners skip all of this
(the owner shortcut covers them). To manage who else may use a pair you already hold:
# Grant a user/group access to ONE pair. objId = "<nameId>_<currencyId>" (composite, NOT the per-row account UUID).
saveAccessRules(
objType="account",
objId="<nameId>_<currencyId>",
rules=[{ "action": "create",
"data": { "saId": <saId>, # or userId / groupId
"privs": { "view": true, "modify": true, "remove": true } } }])
# Grant a WHOLE account category (all currencies of a name) across every actor.
# objId is the account-NAME id prefix — it matches all accounts named "<nameId>_*".
bulkSaveAccountPairsAccessRules(
accId="ws_xxx",
items=[{ "objId": "<nameId>",
"rules": [{ "action": "create",
"data": { "saId": <saId>, "privs": { "view": true, "modify": true } } }] }])
# Inspect who currently has pair access (use the composite pair id, NOT the per-row account UUID):
getAccessRules(objType="account", objId="<nameId>_<currencyId>")
saveAccessRules / bulkSaveAccountPairsAccessRules require you to already hold the pair
(or be the workspace Owner) — they do not self-grant. For your own first access to a brand
new pair, createAccountPair is the only bootstrap.
Notes & gotchas:
- The pair id is
<nameId>_<currencyId> (e.g. c16d1bd0-…-…_933), not the per-row account
UUID that createAccount returns. Using the bare UUID with the access-rule tools also 403s.
- Granting pair access via the rule tools needs access to grant: only the workspace Owner (or
someone who already holds the pair) can seed the first rule that way. For a brand-new pair use
createAccountPair, which self-grants. If a pair already has rules that exclude you, an Owner
must grant you (or make you workspace Owner, which resolves it globally).
- This pair model is why account ops can 403 on a fresh local workspace where the user is only a
Member/Guest, while the same flow works on cloud where the user is the workspace Owner.
Transactions (one account, with history)
# Immediate. `ref` makes it idempotent (safe to retry). Note: `comment`, not `description`.
createTransaction(accountId="<account id>", amount=450, comment="Oil change",
ref="service-jan-2024", data={"invoice": "INV-001"})
# 2-step hold → finalize. Step 1 authorizes; pass a `ref` so you can finalize by it.
createTransaction(accountId="<account id>", amount=500, comment="Pending maintenance",
ref="maint-hold-1", expiration=1735689600) # creates an 'authorized' hold
finalizeTransaction(accountId="<account id>", type="completed", ref="maint-hold-1") # or type="canceled"
# Atomic multi-leg (all-or-nothing) — e.g. double-entry depreciation
atomCreateTransaction(accId="ws_xxx", items=[
{ "accountId": "<asset acc>", "amount": -3000, "comment": "Depreciation debit", "ref": "dep-2023-a" },
{ "accountId": "<depr acc>", "amount": 3000, "comment": "Depreciation credit", "ref": "dep-2023-b" }
])
# Read
getTransactions(actorId="<actor UUID>", currencyId=1, nameId="<aname>", limit=20) # currencyId+nameId required
getAccountTransactions(accountId="<account id>", limit=20, offset=0) # limit+offset BOTH required
getTransactionByRef(accountId="<account id>", ref="service-jan-2024") # confirm a write landed
finalizeTransaction type ∈ completed | canceled (also declined/reversed); identify the
hold by its ref (or parentRef), not a transaction id.
If a createTransaction/atomCreateTransaction here returns HTTP 400 "…is transfer-only",
the account's account-name is flagged transferOnly — use createTransfer instead (see
"Transfer-only categories" under Account names).
Transfers (move value between accounts)
Rule: any movement of value BETWEEN accounts uses a TRANSFER, not a transaction.
createTransaction writes to one account. The moment value moves between two or more
accounts — whether they belong to different actors or are two accounts of the same
actor — use createTransfer (or createTransferTwoStep for a hold). The from legs debit,
the to legs credit, and the whole thing is atomic.
A transfer has from (debit legs) and to (credit legs) — each an array of
{accountId, amount, ...}. It is atomic.
# Immediate
createTransfer(accId="ws_xxx",
from=[{ "accountId": "<source acc>", "amount": 200 }],
to=[{ "accountId": "<dest acc>", "amount": 200 }],
comment="Budget reallocation", ref="transfer-q1")
# 2-step: authorize a hold, then complete/cancel
createTransferTwoStep(accId="ws_xxx", type="authorized",
from=[{ "accountId": "<source acc>", "amount": 1000 }],
to=[{ "accountId": "<dest acc>", "amount": 1000 }],
ref="payout-99")
createTransferTwoStep(accId="ws_xxx", type="completed", transferId="<tr id>") # or type="canceled"
# Read
getTransfer(transferId="<tr id>", filter="id,amount,createdAt,comment")
getTransferByRef(accId="ws_xxx", ref="payout-99")
filterTransfers(accId="ws_xxx", from=1704067200, to=1706745600, # window (unixtime)
incomeType="debit", amount=200, oper="gte", limit=50)
Transfers between users (via their twin actors)
Every workspace user has a system "twin" actor that carries their accounts. To move value
between users (or between a user and any other actor), you transfer between their twin actors'
accounts — there is no "transfer to a userId" call. The flow:
- Find the user(s) by name →
searchUsers(accId, query) (or getUsers) → get each userId.
- Resolve each user's twin actor →
getSystemActor(accId, objType="user", objId="<userId>")
→ the user's actor (and its id).
- Find the account on each twin actor — resolve the account name first, then the currency
(
searchAccountNames→nameId, then searchCurrencies→currencyId), then
getAccounts(actorId=<twin actor>) / getBalance(actorId, nameId, currencyId) to get the
accountId (create it with createAccount if missing).
- Transfer between those accounts with
createTransfer (the from/to legs reference the
twin actors' accountIds).
# "Transfer 100 USD from Olena to Petro on their Wallet accounts"
searchUsers(accId="ws", query="Olena") → userId 4210
searchUsers(accId="ws", query="Petro") → userId 4310
getSystemActor(accId="ws", objType="user", objId="4210") → actor A (Olena's twin)
getSystemActor(accId="ws", objType="user", objId="4310") → actor B (Petro's twin)
getAccounts(actorId="<A>") → Olena's Wallet accountId accO
getAccounts(actorId="<B>") → Petro's Wallet accountId accP
createTransfer(accId="ws",
from=[{ "accountId": "accO", "amount": 100 }],
to=[{ "accountId": "accP", "amount": 100 }],
comment="Olena → Petro", ref="o2p-1")
For more than two parties, add more legs to from/to (the totals must balance), e.g.
split one debit across several credit legs. Multi-account moves on a single actor work the
same way — just reference that actor's several accountIds in the legs.
Counters (Scylla-backed analytical tallies)
Counter accounts are accounts, but a special fast kind: their balances live in ScyllaDB
and they keep no per-transaction history. They are cheap, high-throughput running counts/sums
for analytics and anti-fraud — not an auditable ledger. Use the regular account + transaction
tools when you need history; use the counters API for fast totals.
- Addressed by
(formId, actorRef, accountName, currency, incomeType) — not by account id.
counter = running sum (record deltas via amount); uniqCounter = dedupes by trsRef
(the same event counted twice lands once).
# Record a mileage reading (running counter)
saveCounters(accId="ws_xxx", openAccounts=true, items=[
{ "formId": 42, "actorRef": "car-camry", "accountName": "mileage", "currency": "Km",
"incomeType": "debit", "type": "counter", "amount": 45000 }])
# Override a counter to a fixed value
setCounters(accId="ws_xxx", items=[
{ "formId": 42, "actorRef": "car-camry", "accountName": "mileage", "currency": "Km",
"incomeType": "debit", "amount": 50000 }])
# Read counter values
getCounters(accId="ws_xxx", items=[
{ "formId": 42, "actorRef": "car-camry", "accountName": "mileage", "currency": "Km",
"incomeType": "debit", "type": "counter" }])
Two ways to get a counter, don't confuse them: a counter account created with
createAccount(... counterType="counter") (addressed later by account id / actor+name like any
account) vs the counters API above (saveCounters/getCounters, addressed by
actorRef+accountName, bulk). Both are Scylla-backed and history-less; use the counters API for
fast bulk analytics/anti-fraud counts, createAccount(counterType=…) to attach one to an actor.
End-to-end: car financial tracking
# 1. One-time workspace setup — createAccountPair creates the account-name + currency if
# missing AND grants the caller pair access in one call. Do this for EVERY (name, currency)
# you plan to attach to an actor — never skip it, even if the currency already exists.
createAccountPair(accId="ws", accountName="Purchase Value", currencyName="USD",
symbol="$", precision=2) # → nameId <val>, currencyId 1
createAccountPair(accId="ws", accountName="Depreciation", currencyName="USD")
# → nameId <dep>, reuses currencyId 1
createAccountPair(accId="ws", accountName="Maintenance", currencyName="USD")
# → nameId <mnt>, reuses currencyId 1
createAccountPair(accId="ws", accountName="Mileage", currencyName="Km",
symbol="km", precision=0) # → nameId <mil>, currencyId 2
# 2. The actor (a simulator-actors task) — get its UUID, e.g. searchActors / createActor
car = "<car actor UUID>"
# 3. Attach accounts (the NAME carries the meaning; accountType defaults to fact)
createAccount(actorId=car, nameId="<val>", currencyId=1) # Purchase Value
createAccount(actorId=car, nameId="<dep>", currencyId=1) # Depreciation
createAccount(actorId=car, nameId="<mnt>", currencyId=1) # Maintenance
createAccount(actorId=car, nameId="<mil>", currencyId=2, counterType="counter") # Mileage (counter)
# 4. Record value (find each account's id via getAccounts(actorId=car))
createTransaction(accountId="<acc_val>", amount=25000, comment="Initial purchase", ref="purchase-2023")
createTransaction(accountId="<acc_mnt>", amount=450, comment="Oil change", ref="service-jan-2024")
atomCreateTransaction(accId="ws", items=[ # balanced depreciation entry
{ "accountId": "<acc_val>", "amount": -3000, "ref": "dep-2023-a" },
{ "accountId": "<acc_dep>", "amount": 3000, "ref": "dep-2023-b" }])
setAccountAmount(accountId="<acc_mil>", amount=45230) # odometer reading (counter account)
# 5. Report
getAccounts(actorId=car) # live balances per account
getAccounts(actorId=car, from=1704067200000, to=1706745600000) # turnover for the window (MILLISECONDS)
getAccountTransactions(accountId="<acc_mnt>", limit=50, offset=0)
Rank / filter actors by balance (a simulator-actors tool)
filterActors finds/ranks a form's actors by an account balance in one server-side query:
filterActors(formId=42, accountNameId="<aname>", currencyId=1,
orderBy="balance", orderValue="DESC", withStats=true) # top by balance
filterActors(formId=42, accountNameId="<aname>", currencyId=1, amountFrom=10000) # balance ≥ 10000
amountFrom = balance ≥, amountTo = balance ≤; CURRENT balance only (for turnover use
getAccounts(from, to)). Full filter/data semantics live in simulator-actors.
Reference Documents
| Path | When to read |
|---|
$CLAUDE_PLUGIN_ROOT/docs/entities/accounts.md | Account types, income types, tree calculation, formulas |
$CLAUDE_PLUGIN_ROOT/docs/entities/transactions.md | Transaction states, 2-step flow, atomic transactions |
$CLAUDE_PLUGIN_ROOT/docs/entities/transfers.md | Transfer mechanics, holds, filtering |
$CLAUDE_PLUGIN_ROOT/docs/entities/balances.md | Balance history, credit/debit split |
$CLAUDE_PLUGIN_ROOT/docs/entities/counters.md | ScyllaDB counters, time-series metrics |
$CLAUDE_PLUGIN_ROOT/docs/user-flows/custom-car-form.md | Complete financial-tracking example |
Tips
- Amounts are real decimal values —
amount: 500 is 500 USD; never scale by 10^precision.
- Always call
createAccountPair before createAccount for any (name, currency) you are
about to use on an actor. It creates the account-name and the currency if missing AND grants
you pair access — without it, the next getBalance/createTransaction/transfer 403s on any
non-owner workspace. Prefer it over bare createCurrency + createAccountName.
accountType is the value type (fact default | plan | min/max/avg) and counterType selects amount (default) vs counter/uniqCounter; the account name carries the cash/expense/revenue meaning. There is no asset/liability/income enum. incomeType (debit/credit) is a read/counter dimension, not a createAccount arg.
- Transactions use
comment (not description); pass a stable ref for idempotency and to finalize 2-step holds.
- Transfers use
from/to arrays of legs {accountId, amount} — not fromAccountId/toAccountId.
setAccountAmount sets a fixed balance (correction); createTransaction moves it with history. Prefer transactions when audit history matters.
atomCreateTransaction for entries that must balance (double-entry).
getAccountTransactions requires both limit and offset; getTransactions requires currencyId + nameId.
- Counters API = Scylla tallies, no history (analytics/anti-fraud); distinct from
counter-type accounts.
- Tags & triggers are actors (system forms
Tags / AccountTriggers) linked to accounts via saveAccountActors — which replaces the linked set, so always scope it with formId and resend the full list when adding. Read them back with getAccounts(withTags=true / withTriggers=true); data triggers bind to a data field via saveDataFieldActorsByActor/Form.
- Save tokens with
filter on reads (getAccount, getAccounts, getBalance, getTransactions, getTransfer, getCurrencies, getAccountNames) — a comma-separated field allow-list (filter="id,amount,currencyId"), NOT a row filter.
- Sharing an account (who can view/modify) →
simulator-access; the actor itself → simulator-actors.