Ledger quickstart — four calls to a trial balance
Enable the ledger block, create a practice entity, post one business event, and read the trial balance — four MCP calls, nothing to undo.
Updated 9/12/2026
The ledger block is agent-grade double-entry accounting for your account:
legal entities, books, a chart of accounts with semantic roles, fiscal
periods, posting templates, and an audit trail on every entry. Once it is
enabled, an agent drives it through ledger.* MCP tools on the same
connect0 server you already use.
This page goes from nothing to a balanced trial balance in four calls,
against a practice entity — one created with test: true. Practice
entities are excluded from compliance exports, so you can follow along on
a real account without leaving anything behind that an auditor will see.
You need an account where you are an owner or admin, a billing plan that
includes the ledger block, and an MCP client connected to
https://mcp.connect0.ai (Quickstart). Every call
below takes your account_slug; the examples use acme.
1. Enable the block — connect0_block_enable
Blocks are off until an owner or admin turns them on. Enabling starts metering for the block and adds its tools to your session.
// tool: connect0_block_enable
{
"account_slug": "acme",
"block": "ledger",
"reason": "trying the ledger quickstart"
}
The tool is marked destructive, so your client asks you to confirm before it runs. On success the result reports the block's state and that the session's tool list was refreshed:
{
"block": "ledger",
"enabled": true,
"enabled_at": "2026-09-12T17:04:11.318Z",
"tools_refresh": { "registered": 16, "notified": true }
}
If the plan does not include the block the result is block_not_in_plan
with the upgrade path; enabling an already-enabled block is a no-op that
returns its current state. Re-list tools in your client and the
ledger.* tools appear. (connect0_blocks_list shows every block, its
price in credits, and 30-day usage.)
2. Create a practice entity — ledger.create_entity
One call spins up the whole structure: the entity, its leading book, a
chart of accounts instantiated from a versioned template, and a fiscal
calendar. test: true makes it a practice entity.
// tool: ledger.create_entity
{
"account_slug": "acme",
"legal_name": "Acme Practice Co",
"functional_currency": "USD",
"fiscal_year_end": "12-31",
"framework": "us-gaap",
"chart_template": "us-gaap/smb-v1",
"test": true
}
This one also asks for confirmation — entities are not deletable once
created. The result is the entity summary. Keep two ids from it: the
entity's id and its leading_book_id.
{
"id": "6c1f0f1e-6c1b-4a9d-9c0e-2e0b1b1c4d21",
"legal_name": "Acme Practice Co",
"functional_currency": "USD",
"is_test": true,
"leading_book_id": "b3a7c4e8-1d2f-4f7a-9b6e-5c8d2a1f0e33",
"periods": [
{ "id": "…", "code": "2026-09", "starts_on": "2026-09-01", "ends_on": "2026-09-30", "status": "open" }
],
"accounts": [
{ "id": "…", "code": "1000", "name": "Operating bank account", "role": "asset.cash.bank" },
{ "id": "…", "code": "4000", "name": "Sales revenue", "role": "income.sales" }
],
"opening_transaction_id": null
}
Accounts carry a role (asset.cash.bank, income.sales, …) as well
as a code. Roles are what posting templates and reads refer to, so the
same template works against any chart.
3. Post a business event — ledger.post_event
You never name accounts when you post. You describe what happened — "an order was captured for 120.50" — and a posting template for that event type expands it into the double entry.
A fresh account has no templates yet, so register one for the event type you are about to post. Templates are immutable versions; legs reference accounts by role and amounts are expressions over the event:
// tool: ledger.create_template
{
"account_slug": "acme",
"name": "Cash sale",
"definition": {
"language": "ptl-1",
"eventType": "cash_sale",
"legs": [
{ "role": "asset.cash.bank", "direction": "debit", "amount": { "op": "field", "field": "amount" } },
{ "role": "income.sales", "direction": "credit", "amount": { "op": "remainder" } }
]
}
}
(ledger.test_template expands a sample event against a template and
writes nothing — useful while you are shaping one.)
Now the third of the four calls. Start with dry_run: true to see the
exact journal that would result:
// tool: ledger.post_event
{
"account_slug": "acme",
"entity_id": "6c1f0f1e-6c1b-4a9d-9c0e-2e0b1b1c4d21",
"book_id": "b3a7c4e8-1d2f-4f7a-9b6e-5c8d2a1f0e33",
"type": "cash_sale",
"amount": "120.50",
"unit": "USD",
"occurred_at": "2026-09-12T16:30:00Z",
"effective_date": "2026-09-12",
"description": "Quickstart: first sale",
"provenance": { "rule": "ledger-quickstart" },
"dry_run": true
}
The dry run returns the expanded journal, before/after balances, the
validations that ran, and a short-lived confirmation_token. Re-send
the same payload with dry_run removed and confirmation_token set
to commit. The committed result carries outcome: "posted" and the
transaction:
{
"outcome": "posted",
"event_type": "cash_sale",
"template_code": "cash_sale",
"template_version": 1,
"transaction": {
"id": "0f9a2d3c-…",
"effective_date": "2026-09-12",
"postings": [
{ "account_id": "…", "unit": "USD", "quantity": "120.50", "functional_amount": "120.50" },
{ "account_id": "…", "unit": "USD", "quantity": "-120.50", "functional_amount": "-120.50" }
]
}
}
Two things to know about the write path:
- Provenance is mandatory on every posting. The actor is always your
authenticated identity — you cannot post as somebody else — and the
fields you attach (
model_id,confidence,evidence,rule) go to the audit trail. When a model authored the numbers, say so. - The server decides whether an entry posts. An auto-post policy
looks at the roles, the amount, who is posting and the stated
confidence. Entries it does not admit come back with
outcome: "proposed"— parked for a human to approve withledger.approve_proposal— and nothing has been posted yet.
4. Read the trial balance — ledger.trial_balance
// tool: ledger.trial_balance
{
"account_slug": "acme",
"book_id": "b3a7c4e8-1d2f-4f7a-9b6e-5c8d2a1f0e33"
}
Without period_id the balance is computed as of today over the open
periods; pass a closed period's id to read its immutable, materialised
close instead.
{
"book_id": "b3a7c4e8-1d2f-4f7a-9b6e-5c8d2a1f0e33",
"as_of": "2026-09-12",
"source": "computed",
"lines": [
{ "account_code": "1000", "account_name": "Operating bank account", "role": "asset.cash.bank", "quantity": "120.50", "functional_amount": "120.50" },
{ "account_code": "4000", "account_name": "Sales revenue", "role": "income.sales", "quantity": "-120.50", "functional_amount": "-120.50" }
],
"functional_total": "0"
}
functional_total is 0: the books tie. Amounts are signed decimal
strings, debit-positive, and stay strings end to end — nothing on this
surface is a floating-point number.
Where to go next
ledger.explain_balancedecomposes any figure into the postings, template rules and FX effects that produced it — use it to verify a number before you report it.ledger.list_transactionsandledger.list_accounts(filterable by role, exact or prefix) are the reads an agent uses to orient itself.ledger.get_limitsreports the remaining posting-value budget so an agent can pace itself.- Bringing an existing ledger with you? See Ledger migration recipes.
- Each call is metered in credits; see Credits and pricing.
Ask Zero
Ask a question about connect0 and get an answer grounded in the docs, with links to the sources. Signed in? Zero answers with your account in mind.