# Build your first agent This page builds a working agent in about eighty lines. A person asks when a statutory period ends; the agent answers with the date, the sections it rests on and a hash anyone can replay. The model never counts a day: it calls one domain tool, `period_end`, and the tool asks the canon `de.bgb.fristen@0.1.0` — the period rules of the German Civil Code — through the engine running locally in your process. The model does not see Arxo. It sees a tool named after the task, which is the first pattern of [Choose an integration pattern](/agent-engineering/integration-patterns/). ## What you need - Node 20 or later. - Access to a Claude model for the agent loop: a logged-in Claude Code installation or an `ANTHROPIC_API_KEY` in the environment. - The example directory `examples/first-agent/` from the [example bundle](/agent-engineering/agent-engineering-examples-0.5.0.zip). ```bash cd files/first-agent npm install ``` The four packages are pinned to exact versions: `@arxo/law` 0.3.3 (the engine and the SDK), `@arxo/canon-bgb-fristen` 0.1.5 (the canon, so `open` works offline), `@anthropic-ai/claude-agent-sdk` 0.3.289 (the agent loop) and `zod` 4.6.5 (the tool's input schema). ## Step 1: the domain tool The tool is plain application code; it holds no model and no rule. It turns three arguments into typed facts, asks one question, and returns the engine's answer in the application's own words ([period-tool.mjs](/agent-engineering/files/first-agent/period-tool.mjs)): ```js export function periodTool(canon) { const sections = Object.fromEntries( canon.unfold('frist_ende', { depth: 1 }).tree.producers .map((rule) => [rule.name, rule.anchors?.[0]?.locator ?? null]), ); return async function periodEnd({ event_date, length_days, law_date }) { const answers = [{ predicate: 'frist_ereignis', args: ['frist', event_date] }]; if (length_days !== undefined) { answers.push({ predicate: 'frist_dauer_tage', args: ['frist', `${length_days} calendar_day`] }); } const answer = await canon.collect('frist_ende', ['frist', '?end'], { legalTime: law_date, timezone: 'Europe/Berlin', deadlinePolicy: POLICY, answers, }); // ...status, end dates, grounds with sections, result hash }; } ``` Three choices in it carry the whole method: - **The date of law is an argument.** The tool cannot run without it, and nothing fills it in. - **A missing fact stays missing.** When the user gave no length, the tool sends no length fact; it does not guess one. - **The status always comes back.** The result has `status`, `end_dates` (a list, possibly empty), `grounds` and `result_hash`; an empty list adds a note that it is not an answer. ## Step 2: check the tool without a model Before any model is involved, check that the tool returns what the engine returns. [check-tool.mjs](/agent-engineering/files/first-agent/check-tool.mjs) runs three cases from [expected.json](/agent-engineering/files/first-agent/expected.json): ```bash node check-tool.mjs ``` ```text PASS complete PASS length-missing PASS weekday-end ``` Status: ran locally (`node check-tool.mjs`, Node 20.20.2, `@arxo/law` 0.3.3, `de.bgb.fristen@0.1.0`), offline. The two cases that matter: | Case | Input | Tool result | |---|---|---| | `complete` | event 2026-03-07, 14 days, law read at 2026-09-17 | `COMPUTED`, end date `2026-03-23`, ground `TagesfristEnde` (section 187), hash `sha256:8078d57f…` | | `length-missing` | event 2026-03-07, no length | `COMPUTED`, `end_dates: []`, no grounds, note "do not compute one" | Fourteen days from Saturday 7 March land on Saturday 21 March; the canon's deadline policy moves the end to Monday 23 March. The model is not asked to know that. The second row is the reason the tool exists. The evaluation completed; it found no end date because a fact is missing. `COMPUTED` here does not mean "here is your answer": it means the question was evaluated. Read together with the empty list, it means "ask for the length". The same question asked with `truth` instead of `collect` returns `NEITHER`, and in both forms the answer's `missingInputs` is empty — the engine does not always name the missing fact, so the tool says it plainly. [Read answers](/agent-engineering/handle-answers/) covers every status. ## Step 3: the agent [agent.mjs](/agent-engineering/files/first-agent/agent.mjs) wraps the tool for the Claude Agent SDK and gives the model four instructions: ```js const instructions = `You help people work out statutory periods under German civil law. - Get every end date from the period_end tool. Never count days or move dates yourself. - The tool needs the date at which the law is read. If the user did not state it, ask for it; do not assume today. - If the tool returns no end date, say the law does not derive one from the facts given and name the missing fact. Do not guess. - When you give a date, name the sections in the tool's grounds and quote the result hash.`; for await (const message of query({ prompt, options: { systemPrompt: instructions, model: 'claude-opus-5-5', tools: [], // no built-in tools: only period_end mcpServers: { periods: createSdkMcpServer({ name: 'periods', version: '1.0.0', tools: [periodEndTool] }) }, allowedTools: ['mcp__periods__period_end'], maxTurns: 6, }, })) { /* print the final result, write the trace */ } ``` `tools: []` removes every built-in tool, so the model has nothing to count with except `period_end`. `--out run.json` writes a trace: the prompt, every tool call with its input and result, and the final reply. Run it with the complete facts, then without the length: ```bash node agent.mjs --out run-complete.json \ "A 14-day period starts with an event on Saturday 7 March 2026. Read the law as of 17 September 2026. When does the period end?" node agent.mjs --out run-missing.json \ "A period starts with an event on 7 March 2026. Read the law as of 17 September 2026. When does it end?" ``` Status: NOT RUN for the model loop — the run for this page could not authenticate to a Claude model. The tool underneath ran (step 2). Expected behavior to check in the traces: in the first run one `period_end` call with all three arguments and a reply that gives 23 March 2026, section 187 and the hash; in the second, a call without `length_days` (or none) and a reply that asks for the length instead of giving a date. A third run without a date of law should produce a question for that date and no tool call with an invented one. ## What the model never does - It never counts days: it has no tool to count with. - It never sees `frist_ende`, `frist_dauer_tage` or a deadline policy; the vocabulary of the canon stays inside the tool. - It never turns an empty list into a date. These are properties of the code, not of the prompt: the instructions ask, the tool and the empty tool list enforce. If the model ignores an instruction, the worst it can do is answer without calling the tool — which an evaluation catches by checking that every date in the reply came from a tool result; see [Evaluate, debug and upgrade](/agent-engineering/evaluations/). ## Make it yours - **Another question of the same canon:** add one tool per question. Keep each tool narrow; a tool that takes a free-form predicate hands the canon's vocabulary back to the model. - **Another canon:** open it by `name@version` and read its relations with `unfold` or its question cards before writing the tool. - **A multi-step task:** if the canon publishes a task guide, follow it instead of inventing steps; see [Follow a task guide](/agent-engineering/task-guides/). - **An agent that finds the question itself:** use the Arxo tools over MCP; see [Connect and discover](/agent-engineering/connect-and-discover/). ## Validation record | What | Command | Result | |---|---|---| | Domain tool, three cases | `node check-tool.mjs` | 3 PASS, offline | | SDK behavior used on this page | `collect` and `truth` without the length fact; no deadline policy | empty list with `COMPUTED`; `NEITHER`; without a policy `NON_EXECUTABLE` with an error issue coded `MISSING_POLICY` | | Agent loop | `node agent.mjs …` | NOT RUN: no model authentication in this run | Versions: Node 20.20.2, `@arxo/law` 0.3.3, `@arxo/canon-bgb-fristen` 0.1.5, `@anthropic-ai/claude-agent-sdk` 0.3.289, `zod` 4.6.5. Previous: [Choose an integration pattern](/agent-engineering/integration-patterns/) Next: [The agent contract](/agent-engineering/agent-contract/)