Design partner program is openThree partners, real decision traffic →
razoo

Start

Quickstart

Submit a piece of work, propose an action, see that it did not execute, confirm it as a person would, drain the outbox, and read the decision trail.

This walkthrough uses the reference-brokerage agent that ships in the engine repository, because it already binds the action we will propose. If you are on the SDK path, copy domains/reference-brokerage/ into your host under domains/ first (see Install).

The loop in TypeScript

1. Boot and fix the clock

quickstart.ts
import {
  bootEngine, clockFromIso,
  submitWork, proposeAction, confirmAction, listPending, listDecisions,
  drainOnce, workStatus, explainOf, decisionOf,
} from "@engine/sdk";

// reads ./domains, writes ./data/engine.sqlite
bootEngine({ root: process.cwd() });

const ctx = clockFromIso("2026-09-03T09:00:00.000Z");

Every call takes an EvalContext. It is { now: string } and nothing else. Rules, the kernel, and patterns never read the wall clock; they read ctx.now. That is what makes every decision replayable. wallClock() exists for the moment you genuinely want the current time, but inject a fixed clock in tests.

2. Submit work

const work = await submitWork({
  work_type: "lead",
  source: "quickstart",
  subject: { intake: "Follow up on the Tuesday report" },
  ctx,
});
if (!work.ok) throw new Error(`${work.error}\n${work.valid_example}`);

console.log(work.work_item_id, work.status, work.steps);

submitWork is the same function as POST /api/ingest and engine ingest. The work item walks the plumbing rungs of the process that claims lead (new_business in the reference agent). Ingest never enables the model: work either resolves on deterministic rungs or queues for judgment. The result lists every step, the rung that resolved it, and whether it resolved. An unknown work_type still ingests and queues unassigned; it is kept.

3. Propose an action. It stays pending.

const proposal = proposeAction({
  workItemId: work.work_item_id,
  actionId: "record_intake",
  payload: { note: "Tuesday report" },
  ctx,
});

console.log(proposal);
// { stagedId: "…", discarded: false, queueJudgment: false, status: "pending" }

console.log(listPending(ctx).some((row) => row.id === proposal.stagedId)); // true

This is the invariant that does not change because you embedded the kernel: proposeAction always leaves the row pending. It never auto-confirms, even when the action definition has auto_confirm: true. There is no execute export on the surface at all.

If discarded comes back true, the action was not bound to the govern surface for this agent. See Binding actions below.

4. Confirm, as a person would

const actor = { kind: "human", user_id: "operator-1", email: "operator@example.com" } as const;
confirmAction(proposal.stagedId!, ctx, actor);

Confirm is a separate, explicit act. In your product this call belongs behind the button a person clicks. Confirm re-checks the authority the row was staged under before it marks the row confirmed, and it enqueues staged.confirmed on the transactional outbox in the same SQLite transaction. Still nothing has executed. confirmAction takes an optional trailing actor (and a reason after that), so confirmed_by records who did it; the operator routes pass the session user automatically.

5. Drain the outbox. Now it executes.

const executed = await drainOnce(ctx);
console.log(`drained ${executed} outbox rows`);
console.log(listPending(ctx).length); // the proposal is gone

The transactional outbox is the only job mechanism. drainOnce runs each pending job once; startDrainLoop runs it on an interval and returns a stop function. Inside the staged.confirmed handler the kernel checks that authority once more before any side effect, so a pattern that went stale between confirm and drain does not execute. A failing handler retries up to 8 times, then the row is marked dead_letter and stays visible.

6. Read the trail

const status = workStatus(work.work_item_id, ctx);
console.log(status?.status, status?.kind, status?.blocked_reason);

// list this work item's decisions, newest first
const page = listDecisions({ workItemId: work.work_item_id, ctx });
const decisionId = page.rows[0]?.id;

// rung, artifacts, tokens, provider, model
const decision = decisionOf(decisionId, ctx);
// resolved_by, why, cost, envelope, VAA fields
const why = explainOf(decisionId, ctx);

explainOf returns the rung, the rule or pattern that fired with its version hash, whether a model was used, tokens and list-price cost, the human confirmation, and on a learned fire the pattern evidence count and the VAA lifecycle, standing, and envelope. listDecisions is cursor-paginated (limit, cursor); omit workItemId and pass since instead for every decision after a point in time.

Binding actions: the govern.propose overlay

The kernel refuses to stage an action that the resolving skill has not declared. For the SDK the resolving skill is govern.propose, and its core definition binds nothing. Each agent opts in by adding an overlay entry to domains/<agent>/skills.yaml naming the action ids the govern surface may stage. This is the file that makes step 3 work for reference-brokerage:

domains/reference-brokerage/skills.yaml
skills:
  - id: govern.propose
    allow_llm: false
    max_tokens: 0
    tier: fast
    purpose: External proposals staged through the govern surface
    action_ids: [record_intake]

Only action_ids is read from the overlay; purpose, allow_llm, and tier stay core. Every id must exist in the merged action registry (core plus the agent's actions.yaml), and doctor errors if one does not. A missing overlay means nothing is stageable and every proposal comes back discarded: true. Full rules are in Authoring agents.

The same loop from the CLI

The CLI runs inside the engine repository (or the appliance container) against the same SQLite file.

lead.json
{
  "work_type": "lead",
  "source": "cli",
  "subject": { "intake": "Follow up on the Tuesday report" }
}
Terminal
# same validation and processEvent path as POST /api/ingest
pnpm engine ingest lead.json

# outbox catch-up: runs scheduled jobs and every pending row once
pnpm engine drain

The ingest body is capped at 64 KB, subject at depth 8 and 64 keys. Identity keys, any action_id, and prototype keys are stripped before the ladder sees the subject. The CLI is local and does not need the HTTP ingest token.

There is no CLI command to propose or confirm. Proposing is an SDK call (or a reasoning step inside a process); confirming is a person in the operator console (POST /api/staged/:id/confirm) or your own UI calling confirmAction.

Where to go next

  • Concepts: the ladder, step kinds, envelopes, and proof-state binding.
  • Authoring agents: every file in a domain folder with real excerpts.
  • SDK reference: every export with its signature and errors.