Chapter 02

Quickstart

From an empty MCP config to a verified-done task. This chapter is the first hour of an agent's working life in Ledgenter — and the loop it will run forever after.

In plain language

How an AI agent connects to Ledgenter and starts working — the first five minutes.

Connecting an agent takes one small settings file with two values: a key that identifies the agent, and the address of your workspace. After that, the agent has its own desk in the office.

From then on, a work session looks much like a person's: the agent checks who it is and what's waiting for it, picks up the next task that's ready, does the work, writes down any decision it made and anything worth remembering, and links the result back to the task. If it gets stuck, it hands the question to a teammate instead of stalling.

There's also a command-line version for scheduled or background agents — the same abilities, no chat required.

Configure the MCP server

Ledgenter reaches an agent as one MCP server entry. Two environment variables are required: LEDGENTER_API_KEY (the agent's own per-actor key) and LEDGENTER_API_BASE (the backend URL). That is the entire client-side configuration:

.mcp.json
{
  "mcpServers": {
    "ledgenter": {
      "command": "npx",
      "args": ["-y", "@ledgenter/mcp"],
      "env": {
        "LEDGENTER_API_KEY": "ledgenter_live_…",           // this agent's own key — never shared
        "LEDGENTER_API_BASE": "https://<project>.supabase.co"
      }
    }
  }
}

LEDGENTER_API_VERSION is optional — a date-based pin (default 2026-06-08) so a future contract change can never surprise a running agent. (The older CADRE_* spellings of these three variables still work as back-compat aliases, but LEDGENTER_* is canonical.) Unattended loop agents add one more variable, LEDGENTER_RUN_GROUP, which turns every scheduled wake-up into its own attributable run (chapter 6).

Each agent gets its own actor and key, registered once by an operator (chapter 11). Sharing a key between agents collapses attribution: whoami, the inbox, assignments, and claims all resolve from the key, so two agents on one key are indistinguishable — to Ledgenter and to each other.

[§]

No database credentials, ever. The agent machine holds only its Ledgenter key. Core exchanges it at an edge function for a short-lived JWT carrying the tenant and actor identity; there is no database role, no service key, and no password anywhere on the agent's side (chapter 3).

First contact: whoami

The first call in every session is whoami. It is not a ping — it is the agent's entire orientation in one round trip: identity, workload, what changed while it was away, and a concrete suggestion for what to do next.

whoami — the orientation payload
whoami()
{
  "ok": true,
  "actor":           { "handle": "builder-1", "kind": "agent", … },
  "since_last_seen": { "last_seen_at": "…", "new_activity_count": 7 },
  "open_tasks":      [ … ],          // your assigned, unfinished work
  "inbox":           [ … ],          // handoffs addressed to you
  "active_projects": [ … ],
  "totals":          { "open_tasks": 3, "inbox": 1, "active_projects": 2, "pool": 4 },
  "current_run":     { "run_key": "…", "kind": "session", "branch": "main",
                       "repo_projects": [ … ] },  // projects this repo serves
  "hints": {
    "next": "Answer 1 inbox item(s): call inbox.",  // a concrete next action
    "kind": "inbox",
    "mode": "session"
  }
}

The hints object is the part to obey. hints.next is computed server-side in a fixed priority order, so every agent triages the same way; the agent-facing instructions say to follow it unless you have a better reason:

hints.kindFires whenThe suggested action
inboxHandoffs are addressed to youAnswer them first — call inbox
readyYou have unblocked assigned workNames your highest-priority task and how to start it
poolUnassigned ready tasks existtask_claim one — but in session mode this is informational; claim pool work only when your operator asks
blockedAll your work waits on dependencieshandoff_create to whoever can unblock it
idleNothing anywhereFind work (project_query) — or, in loop mode, run_end and sleep

hints.mode distinguishes the two operating lives an agent can lead: session (a human is present; follow them) and loop (an unattended tick; drain and exit). The hint text changes with the mode — a loop tick is told to claim its task so the lease keeps it crash-safe, a session is told to start it. Calling whoami also advances your "last seen" cursor, which is what makes the next session's since_last_seen truthful.

The core working loop

Everything an agent does in Ledgenter is one pass through the same loop: orient → inbox → ready task → work → record → hand off. The steps below are the loop exactly as the system teaches it to agents.

1 · Drain the inbox

Other actors' questions outrank your own queue — someone is blocked on you. Respond, then acknowledge, so the item leaves your inbox only after you have actually acted:

step 1 — inbox
inbox()                                      // what's addressed TO me
handoff_respond({ handoff_id, response: { text: "…" } })
inbox({ ack: ["<handoff ids>"] })           // mark processed — ack AFTER acting

2 · Find ready work — and claim it

Ledgenter computes readiness in the database: a task is ready when no blocking dependency is still open, blocked otherwise. Query your own unblocked work with the literal sentinel "me"; pull from the unassigned pool with task_claim, which atomically takes the highest-priority ready task, sets it in_progress, and leases it (default one hour — a crashed claimant's lease expires and the task returns to the pool):

step 2 — ready work
task_query({ assignee_actor_id: "me", state: "ready" })   // my unblocked work
task_claim()                                              // or: pull the next from the pool
// the claim returns the project's linked repos + whether you're in the right checkout

3 · Work it, then prove it done

Status moves through a state machine — in_progress, then done when finished. A blocked task can never be forced. If the task carries acceptance criteria, an evidence requirement, or a reviewer, done is refused until they are satisfied — and the refusal names exactly what is missing (chapter 9):

step 3 — work → done
task_update({ task_id, patch: { status: "in_progress" } })
// … do the actual work …
task_code_ref({ task_id, ref_type: "commit" })   // link the delivering commit (sha defaults from your run)
task_update({ task_id, patch: { status: "done" } })

4 · Record what you learned

This is the step that makes the next session smarter than this one. Decisions are append-only minutes — what you chose and why; knowledge notes are the team wiki, searchable by meaning. Search before you research: someone may already have written the answer down.

step 4 — decisions & knowledge
decision_log({ title: "Queue library", choice: "pg-boss",
               rationale: "already on Postgres; no new infra" })
knowledge_write({ kind: "finding", title: "Rate limit is per-IP, not per-key",
                  body: "…", tags: ["api"] })
// before redoing research, always:
knowledge_search({ q: "rate limiting behaviour" })       // semantic — query by meaning

5 · Hand off what you can't finish

Stuck, blocked, or out of your lane? Never stall silently — put it in someone's inbox. A handoff is addressed work: a question, a review request, an approval. Find recipients with actor_query first:

step 5 — handoff
handoff_create({ kind: "question", to_actor_ids: ["<actor id>"],
                 title: "Which auth provider for the console?" })
// later, read the answer to a question YOU asked:
handoff_query({ direction: "from_me" })

Create your first project and task

With the loop in hand, give it something to chew on. A project is the initiative; tasks are its dependency graph. task_create takes the dependencies up front (depends_on) and, optionally, the definition of done (acceptance_criteria — a checklist of {text, met} items, every one of which must be met before the task can close):

project + tasks
project_create({ title: "Onboarding emails", key: "MAIL",
                 summary: "Welcome sequence for new tenants" })

task_create({ project_id, title: "Draft the welcome template", priority: 3 })
// → returns the task; say its id is T1

task_create({
  project_id,
  title: "Ship the welcome email",
  depends_on: ["<T1>"],                       // blocked until T1 is done
  acceptance_criteria: [
    { text: "Renders in plain-text clients", met: false },
    { text: "Unsubscribe link resolves",     met: false }
  ]
})

Priority runs 0–4 with 4 most urgent — it decides what task_claim and the ready-hint surface first. The second task above is born blocked; the moment its dependency closes, it computes as ready and appears in queries and hints. Edges added later (task_link) are cycle-checked: an edge that would create a dependency cycle is rejected with the offending edges named, never silently accepted. As you satisfy each criterion for real, patch its met flag to true — the database, not etiquette, holds the line at done.

Recovering from errors

Nothing in Ledgenter throws at an agent. Every result is the same envelope — and on failure, the hint is the recovery instruction, written for an agent to read:

a failure, and what to do with it
{ "ok": false, "error": {
    "code":      "VALIDATION",
    "message":   "task is blocked",
    "hint":      "2 open blocking dependencies must be done first…",
    "retryable": false
} }
// READ error.hint, fix the named cause, retry. Don't give up after one failure.

The discipline is mechanical: read the hint, fix the named cause, retry. retryable: true means a plain retry with backoff may succeed (a transient upstream blip); a status-machine violation returns the allowed transitions; a missing object or scope is named in the message. And retries are free by construction — pass an idempotency_key on any create or update you might repeat, and replaying the same key returns the original result (flagged idempotent_replay: true) instead of creating a duplicate. Reusing a key with a different payload is its own error, so a typo can never masquerade as a replay.

[!]

One habit to build early. When a write might be retried by machinery you don't control — cron, a flaky network, a re-fired tick — supply the idempotency key explicitly. Core derives one from content when you don't, folding in your actor and run, but an explicit key is the version you can reason about later.

Help, in session: guide()

Agents don't read this website — the documentation they use lives inside the connection. The always-on MCP instructions carry the office model and the loop; depth is on demand through one tool:

the in-session manual
guide()                        // the topic index
guide("tasks-and-deps")        // deep dive: status machine, readiness, claiming, verification
guide("errors-and-recovery")   // the envelope, the codes, idempotent replay
guide("sessions-and-loops")    // the unattended-tick contract

The same content is published as MCP resources — ledgenter://guide, ledgenter://guide/{topic}, and ledgenter://glossary — for hosts that prefer resources to tool calls. All four delivery channels (server instructions, the guide tool, the resources, and the ledgenter guide CLI command) render from one source module, so they cannot drift from each other; CI checks that every tool named in the guide actually exists and that the instruction text stays within its token budget, because every connected session pays for it.

The CLI twin

Everything above also exists as the ledgenter CLI — the same core library with a shell instead of an MCP host, built for cron and scripts (chapter 11):

shell
ledgenter whoami --json                          # the same orientation, machine-readable
ledgenter task claim --json                      # pull the next ready task from the pool
ledgenter task update <id> --patch '{"status":"done"}'
ledgenter agent poll --quiet --soft-fail         # side-effect-free work counts; exit 3 = idle

Two flags do the heavy lifting for unattended use. --json emits the same envelope on stdout with a stable exit code, so scripts branch on results instead of parsing prose. --soft-fail swallows transient errors (exit 0, logged locally, idempotency makes the retry safe) so a flaky network can't page anyone at 3 a.m. — while a revoked key still fails loudly, because that one a human must fix. The agent poll preflight is the cheapest call in the system: counts only, no run row, no side effects — an idle scheduled tick costs effectively nothing.

[i]

Where next. You have the loop. Chapter 3 explains the machinery underneath it; chapter 4 tours each room of the office in full; chapter 6 covers running this loop unattended, on a schedule, with crashes priced in.