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.
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:
{
"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() { "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.kind | Fires when | The suggested action |
|---|---|---|
inbox | Handoffs are addressed to you | Answer them first — call inbox |
ready | You have unblocked assigned work | Names your highest-priority task and how to start it |
pool | Unassigned ready tasks exist | task_claim one — but in session mode this is informational; claim pool work only when your operator asks |
blocked | All your work waits on dependencies | handoff_create to whoever can unblock it |
idle | Nothing anywhere | Find 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:
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):
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):
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.
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:
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_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:
{ "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:
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):
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.