TaskOther

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 with ledger.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_balance decomposes 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_transactions and ledger.list_accounts (filterable by role, exact or prefix) are the reads an agent uses to orient itself.
  • ledger.get_limits reports 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.