Getting Started with Realm
This guide walks through building, running, and testing a Realm workflow from scratch.
Time to complete: ~10 minutes
Prerequisites: Node.js 22+, npm 10+
1. Install the CLI
Section titled “1. Install the CLI”npm install -g @sensigo/realm-cliVerify the install:
realm --version2. Scaffold a Workflow
Section titled “2. Scaffold a Workflow”realm workflow init extraction-democd extraction-demoThis creates:
extraction-demo/ workflow.yaml # workflow definition schema.json # optional shared params schema .env.example # secrets template README.md # project readme3. Understand the Workflow Definition
Section titled “3. Understand the Workflow Definition”Open workflow.yaml. The key fields are:
id: extraction-demo # unique identifier used in all CLI commandsname: 'Extraction Demo'version: 1
steps: step_one: description: '...' execution: agent # the AI agent executes this step depends_on: [] # no dependencies — starts immediately input_schema: # the engine validates agent output against this schema type: object additionalProperties: false required: [result] properties: result: type: string
finalize: description: '...' execution: auto # engine executes this automatically depends_on: [step_one]execution: agent — the step waits for the AI agent (or realm workflow run in dev mode) to call execute_step.
execution: auto — the engine runs this step immediately, optionally calling a registered handler.
trust: human_confirmed — the engine pauses and waits for submit_human_response before advancing.
For a complete reference of all step fields, execution modes, transitions, and agent profiles, see YAML Schema Reference.
4. Validate and Register
Section titled “4. Validate and Register”realm workflow validate ./ # parse and validate the YAMLrealm workflow register ./ # store the workflow definition locallyrealm workflow validate catches schema errors, duplicate step IDs, and invalid depends_on references before you run anything.
Development tip — auto-register on save: instead of running realm workflow register after every edit, use realm workflow watch:
realm workflow watch ./ # registers immediately, then re-registers on every file changeMake edits to workflow.yaml freely — each save triggers a re-registration. Invalid YAML is logged but does not stop the watcher; fix the file and save again to recover. Press Ctrl+C to stop.
5. Run Interactively
Section titled “5. Run Interactively”realm workflow run ./realm workflow run is a development driver. For each agent step it prompts you to type the JSON output. For human gates it prompts for approval. This lets you exercise the full workflow without an AI agent.
Example session:
Step: step_one — Collect the required informationEnter agent output (JSON): {"result": "the document text"}
Step: finalize — Human reviews and approvesApprove? [y/n]: y
Run completed. Phase: completedRun ID: abc1236. Run with realm agent
Section titled “6. Run with realm agent”realm workflow run (section 5) manually drives each step by prompting you to type JSON — useful for learning the state machine without setting up an LLM. realm agent does the same thing end-to-end with a real LLM, in a single terminal command, with no MCP client or IDE required:
realm agent \ --workflow ./extraction-demo \ --params '{"path":"input.txt"}'Set OPENAI_API_KEY or ANTHROPIC_API_KEY before running. Use --provider anthropic to switch providers.
realm agent registers the workflow temporarily, starts a run, and drives every execution: agent step. Auto steps run without any LLM call. Schema validation applies exactly as in the interactive run — if the LLM returns invalid output, the engine rejects it, the agent retries, and the run only advances when the schema passes.
If the workflow contains a human gate, realm agent pauses and prints the gate message. When no Slack bot integration is configured, it also prints a realm run respond command for each choice — run it in a second terminal to submit your response. When a notifiers.slack_gate block with bot_token + channel_id is configured in your deployment manifest (realm.yaml), the terminal commands are suppressed — resolve the gate directly in Slack. realm agent detects the resolved gate in either case and continues automatically.
Slack gate notifications: instead of responding in the terminal, you can have realm agent post the gate message to Slack. Configure notifiers.slack_gate in your deployment manifest (realm.yaml): webhook_url alone gives one-way notification (terminal command still required); bot_token + channel_id give full bidirectional resolution — reply in the Slack thread and the gate resolves automatically. A third mode using the Slack Events API gives real-time push delivery for production deployments. For setup instructions, see Slack Gate Modes.
When the run completes, realm agent prints the last agent step’s output directly to the terminal:
Run complete: dc8b8c11-48db-4613-85b8-9c4dff1681f8
Result (step_one):{ "result": "the document text"}To also persist the workflow definition so realm run inspect and realm run list resolve it by ID, pass --register:
realm agent \ --workflow ./extraction-demo \ --params '{"path":"input.txt"}' \ --registerrealm agent vs MCP: use realm agent for standalone runs — CI pipelines, quick local testing with a real LLM, scripts that must complete without an IDE. Use MCP (section 12) when an AI agent embedded in an IDE should drive workflows as part of a broader interactive session.
7. Inspect the Evidence Chain
Section titled “7. Inspect the Evidence Chain”After a run completes (or fails), inspect the full evidence chain:
realm run inspect abc123The output shows every step in execution order:
Run: abc123Workflow: my-workflow v1State: completed ✓Created: 2026-01-15T10:30:00.000ZUpdated: 2026-01-15T10:30:42.000Z
Evidence (2 steps):
1. gather_input [profile: default] success 4231ms hash: a1b2c3d4 Input: {"topic":"quarterly earnings"} Output: {"summary":"Revenue grew 12% YoY...","sources":3} Diagnostics: ~900 tokens | no preconditions
2. write_report [profile: default] success 6104ms hash: e5f6a7b8 Input: {"summary":"Revenue grew 12% YoY...","sources":3} Output: {"report":"# Q4 Earnings Report\n## Summary\nRevenue grew 12% YoY...","word_count":412} Diagnostics: ~1600 tokens | preconditions: gather_input.result.summary != "" → true (Revenue gr…)Each evidence entry records:
- Input / Output — what the step received and what it returned, truncated to 120 characters.
- Hash — first 8 characters of the SHA-256 chain hash. The hash changes if any prior step’s output changes, making the chain tamper-evident.
- Diagnostics — token estimate (context window size) and the precondition trace (each precondition expression, pass/fail, and the resolved value).
Debugging with inspect:
If a step fails, its Output field contains the error or the unexpected value. If a
precondition blocked a step, the precondition_trace shows the exact expression and the
value that caused it to fail. See the CLI reference
for the full field guide and diagnostic patterns.
8. Adding a Service
Section titled “8. Adding a Service”Services let the engine fetch, create, or update data in external systems.
Built-in: FileSystemAdapter
Section titled “Built-in: FileSystemAdapter”@sensigo/realm ships FileSystemAdapter, which reads a local file and returns
{ content, path, line_count, size_bytes }. When using createRealmMcpServer() with no
custom registry, it is pre-registered automatically under the name filesystem — no
TypeScript required. Declare it only in your workflow YAML. For the full operations and
response reference, see Built-in Service Adapters.
services: filesystem: adapter: filesystem trust: engine_delivered # SERVICE trust level — a different key from a step's own 'trust:'
steps: read_file: description: Read the input file execution: auto depends_on: [] uses_service: filesystem operation: readThe file path is taken from the run’s params — declare it in params_schema and pass it when
calling start_run. The step result is injected directly into the evidence; the agent cannot see
or alter it. See examples/03-incident-response/ and examples/02-ticket-classifier/ for working examples.
When you provide a custom registry (e.g. to register a handler), auto-registration does not
apply — start from createDefaultRegistry() from @sensigo/realm-mcp and add your extensions
on top, so built-in adapters remain available:
import { createDefaultRegistry } from '@sensigo/realm-mcp';
const registry = createDefaultRegistry();registry.register('handler', 'my_handler', myHandler);
const server = createRealmMcpServer({ workflowStore, registry });GitHubAdapter
Section titled “GitHubAdapter”@sensigo/realm also ships GitHubAdapter, which speaks to the GitHub REST API (and GitHub
Enterprise Server). It is not pre-registered — declare it under adapters: in your deployment manifest with config: { auth: { token: "${secret:GITHUB_TOKEN}" } }
in your environment and the CLI registers it automatically. For the full operation list
(get_pr_diff, get_issue, post_comment, apply_labels, and more), auth token scope
requirements, GitHub Enterprise setup, and the MCP server registration pattern, see
Built-in Service Adapters — GitHubAdapter.
Custom adapters
Section titled “Custom adapters”To fetch from any other source — an API, a database, a cloud service — implement ServiceAdapter
and register it the same way. GenericHttpAdapter (also in @sensigo/realm) covers most REST
APIs without any custom code — see the adapter reference.
For fully custom logic, add the service declaration to workflow.yaml:
services: source: adapter: google_docs trust: engine_delivered # SERVICE trust level — a different key from a step's own 'trust:'
steps: fetch_document: description: Fetch the document from Google Docs execution: auto depends_on: [] uses_service: sourceWhatever a service’s trust is declared as (a different key from a step’s own trust: — see Trust levels), the response is injected directly into the step’s evidence and the agent never sees or alters it — realm does not yet branch on this value at all (#530).
Implement the adapter in TypeScript and register it with ExtensionRegistry:
import type { ServiceAdapter, ServiceResponse } from '@sensigo/realm';import { ExtensionRegistry } from '@sensigo/realm';
const googleDocsAdapter: ServiceAdapter = { id: 'google_docs', async fetch(_operation, _params, config): Promise<ServiceResponse> { const token = config['token'] as string; // call the Google Docs API … return { status: 200, data: { text: '…' } }; }, async create(_op, _p, _c) { return { status: 501, data: {} }; }, async update(_op, _p, _c) { return { status: 501, data: {} }; },};
const registry = new ExtensionRegistry();registry.register('adapter', 'google_docs', googleDocsAdapter);9. Adding a Human Gate
Section titled “9. Adding a Human Gate”A human gate pauses a run and requires explicit approval before the engine advances to the next step. Add one to workflow.yaml:
steps: review_findings: description: 'Security team reviews the identified findings.' execution: auto trust: human_confirmed depends_on: [analyze_findings] gate: choices: - approve - reject message: | {{ context.resources.analyze_findings.finding_count }} findings identified. Severity: {{ context.resources.analyze_findings.highest_severity }}. Reply 'approve' to accept or 'reject' to flag for re-review. resolution_messages: approve: 'Approved — run continuing.' reject: 'Rejected — run stopped.'Choosing your gate choices: keep them short, unambiguous, and domain-relevant. Common patterns:
approve/reject— clear for most approval flowsyes/no— minimal and obvioussend/discard— for outbound content flowsapprove/reject/postpone— three-way with a deferral path
Each choice routes independently: use when: "gate_step.choice == 'reject'" on downstream steps to branch based on the outcome. Choices must be typed exactly when resolving the gate — the engine does not interpret synonyms or partial matches.
gate.message is optional. When set, it is resolved at gate-open using the step’s output for template substitution — including self-reference ({{ context.resources.review_findings.field }} reads the gate step’s own output). If any {{ }} placeholder cannot be resolved, the engine returns an error and the gate does not open. The resolved message is stored permanently in the evidence chain as a verbatim record of what the human saw.
gate.resolution_messages is optional. When set, the value for the chosen key is displayed as a confirmation after the gate resolves, instead of the default ✅ Gate resolved: \choice` — run continuing.`.
When the engine reaches a step with trust: human_confirmed, it pauses the run and returns status: confirm_required. The run will not advance until a human responds.
To resume a paused run from the CLI:
realm run respond <run-id>To resume via the MCP tool: call submit_human_response with the run_id, gate_id, and choice from the confirm_required response.
Gate response fields
Section titled “Gate response fields”When the engine opens a gate, the confirm_required response includes a gate object:
gate.display— the human-facing content. Resolved fromgate.messageif configured; otherwise falls back tostep.promptresolved. Present this to the user verbatim before asking for their choice.gate.agent_hint— optional agent protocol instruction resolved fromstep.instructions. If present, the agent follows this to determine how to present the gate.gate.response_spec.choices— the valid choice values (e.g.["approve", "reject"]). The human must reply with one of these values exactly. Pass the chosen value aschoicewhen callingsubmit_human_response.gate.preview— the full step output at point of gate opening, for reference and debugging.
Authoring constraint: keep step.prompt content bounded and human-readable — a concise summary, a count, or a short excerpt. Do not surface unbounded payloads (full document text, large JSON objects) as the gate display. Workflow authors are responsible for ensuring gate content remains manageable.
10. Adding a Step Handler
Section titled “10. Adding a Step Handler”A step handler contains business logic for an execution: auto step — validation,
transformation, enrichment, or any computation the engine should run automatically.
steps: extract_fields: description: 'Extract structured fields from the input.' execution: agent depends_on: []
validate_output: description: 'Validate that required fields are present.' execution: auto handler: check_required_fields depends_on: [extract_fields] config: required_keys: [name, date, summary]
handle_validation_error: description: 'Recovery step — invoked only when validation fails.' execution: agent depends_on: [validate_output] trigger_rule: one_failedThe handler: value is the name string. The config: block is a freeform key-value object
delivered to the handler as context.config. Add any static configuration your handler needs
here. Recovery on handler failure is declared on a downstream step using trigger_rule: one_failed
or all_failed — there is no inline transitions: block.
Writing a handler
Section titled “Writing a handler”Import and implement the StepHandler interface from @sensigo/realm:
import type { StepHandler, StepHandlerInputs, StepContext, StepHandlerResult,} from '@sensigo/realm';
const checkRequiredFields: StepHandler = { id: 'check_required_fields',
async execute(inputs: StepHandlerInputs, context: StepContext): Promise<StepHandlerResult> { const keys = (context.config['required_keys'] as string[] | undefined) ?? []; const fields = inputs.params as Record<string, unknown>; const missing = keys.filter((k) => !(k in fields) || fields[k] === null || fields[k] === ''); if (missing.length > 0) { throw new Error(`Missing required fields: ${missing.join(', ')}`); } return { data: { validated: true, field_count: keys.length } }; },};Key rules:
- Throw a plain
Error— the engine wraps it asENGINE_HANDLER_FAILED. Do not import engine internals. - Return
{ data: { ... } }for business-logic outcomes (e.g. “no matches found”). Only throw for genuine errors the workflow cannot proceed from. - Read
context.configfor step-level configuration,context.resources['step_name']['field']for prior step outputs, andinputs.paramsfor the agent’s submitted input from the previous step. - If your handler does async I/O, check
signal?.abortedbetween operations and throw if true.
Accessing prior step outputs
Section titled “Accessing prior step outputs”import { resolveResource } from '@sensigo/realm';
// Reads context.resources['fetch_document']['text'], returns undefined if missingconst text = resolveResource(context.resources, 'fetch_document', 'text');if (typeof text !== 'string') { throw new Error('source text missing');}Registering a handler
Section titled “Registering a handler”import { ExtensionRegistry } from '@sensigo/realm';import { createRealmMcpServer } from '@sensigo/realm-mcp';
const registry = new ExtensionRegistry();registry.register('handler', 'check_required_fields', checkRequiredFields);
const server = createRealmMcpServer({ registry });Available primitives
Section titled “Available primitives”@sensigo/realm exports five utility functions you can compose inside handlers:
| Function | Purpose |
|---|---|
resolveResource(resources, stepId, field) |
Read a field from a prior step’s output. Returns undefined on missing. |
walkField(data, fieldName) |
Recursively collect all objects containing fieldName from a nested structure. |
partitionBySubstring(candidates, quoteField, sourceText) |
Split candidates into accepted/rejected by verbatim substring presence. |
countResults(accepted, rejected) |
Compute { accepted_count, rejected_count, candidates_found }. |
compareStrings(a, b, mode) |
Compare strings with "exact", "prefix", or "regex" mode. |
Built-in handlers
Section titled “Built-in handlers”Two handlers are available without registration:
validate_verbatim_quotes— verifies AI-extracted quotes appear verbatim in a source document. Detects hallucinations.validate_field_match— reads a field from a prior step and compares it to a pattern. Guards that a fetched resource belongs to the expected entity.
For full interface documentation, context field reference, handler composition patterns, built-in handler config/output tables, and testing utilities, see the Handler Authoring Reference.
---
## 11. Adding Agent Profiles
An agent profile is a Markdown file that defines a persona for an `execution: agent` step. The profile content is delivered to the AI agent verbatim as `agent_profile_instructions` when the step is reached, before it submits any output.
### Create a profile file
Add a `profiles/` directory next to your `workflow.yaml`:
```extraction-demo/ workflow.yaml profiles/ extractor.md```
Write the persona in plain Markdown — no special syntax required:
```markdownYou are a precise document extraction specialist.
Your output must be structured exactly as defined in the step schema.Do not paraphrase. Do not add context. Quote verbatim or omit entirely.```
### Reference the profile in a step
```yamlsteps: step_one: description: Extract key facts from the document execution: agent depends_on: [] agent_profile: extractor # loads profiles/extractor.md```
The `profiles_dir` top-level field defaults to `profiles/` relative to the workflow YAML. Set it explicitly if your profiles live elsewhere:
```yamlprofiles_dir: ./shared/personas # relative to workflow.yaml```
### Register to apply changes
Profile content is read from disk at **registration time** and baked into the stored workflow definition. The AI agent receives the content over MCP at runtime — no file system access is needed on the server.
```bashrealm workflow register ./extraction-demo# ✓ Registered: extraction-demo v2 (2 steps)```
If any referenced profile file is missing, registration fails immediately with the expected path in the error message.
> **Re-registration required for profile updates.** If you edit a profile file after registering, run `realm workflow register` again. The server continues serving the previously baked content until you do.
Multiple steps can share the same profile name — Realm reads the file once and records a single hash.
For the complete field reference, see [Agent profiles](reference/yaml-schema.md#agent-profiles) in the YAML Schema Reference.
---
## 12. Connect an AI Agent via MCP
The MCP server is built into the `realm` CLI — no extra install needed if you already have`@sensigo/realm-cli` installed globally. Start it with:
```bashrealm mcp```
Configure your AI client. **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
```json{ "mcpServers": { "realm": { "command": "realm", "args": ["mcp"] } }}```
**Cursor** (`~/.cursor/mcp.json`):
```json{ "mcpServers": { "realm": { "command": "realm", "args": ["mcp"] } }}```
For hosted platforms that cannot spawn a local subprocess, use `realm serve` instead. See the[CLI Reference](reference/cli-commands.md#realm-serve) for details.
The agent has access to 10 MCP tools:
| Tool | Description || ----------------------- | ------------------------------------------------------------------ || `list_workflows` | List all registered workflows || `get_workflow_protocol` | Get step-by-step instructions for a workflow || `start_run` | Start a new run || `start_run_batch` | Start many runs of one workflow in a single call || `execute_step` | Submit output for the current agent step || `submit_human_response` | Approve or reject a human gate || `get_run_state` | Inspect the current state of a run || `abandon_run` | End a non-terminal run without completing it || `create_workflow` | Register a dynamic workflow at runtime and immediately start a run || `append_trace` | Add trace entries during a step, before submitting it |
The agent should call `list_workflows` first to discover what is registered, then`get_workflow_protocol` for the matched workflow before calling `start_run`. The protocol isembedded in each workflow definition and provides exact instructions for what to do at each step.
### Mode 2: self-directed execution with `create_workflow`
When no pre-registered workflow matches the task, an agent can define its own multi-step plan at runtime:
```create_workflow steps: - id: research_problem description: "Audit all JSDoc comments in the repository and list files with missing or inaccurate docs." - id: generate_fixes description: "For each file identified in the previous step, generate corrected JSDoc." input_schema: type: object additionalProperties: false properties: audit_summary: { type: string } required: [audit_summary] metadata: name: jsdoc-audit task_description: "Audit and fix JSDoc across the codebase."```
`create_workflow` registers the workflow and starts a run in one call — no YAML file, no `realm workflow register`. The response includes `data.workflow_id` and `next_actions` pointing at the first eligible step. The agent then uses `execute_step` exactly as it would for a YAML workflow.
Runs created by `create_workflow` carry the same evidence chain and `next_actions` guidance as YAML-registered runs. See [`.github/instructions/realm-create-workflow.instructions.md`](../.github/instructions/realm-create-workflow.instructions.md) for the full protocol.
### Using multiple workflows
Realm supports any number of registered workflows in one store. To add more workflows:
```bashrealm workflow register ./my-other-workflow```
When the agent calls `list_workflows`, it receives the full list of registered IDs and names. Itmatches the user's request to the right workflow ID, calls `get_workflow_protocol` to retrievethat workflow's step-by-step briefing, then proceeds with `start_run`.
For VS Code, place a generic `.github/instructions/realm.instructions.md` in your repository(see [examples/03-incident-response/](../examples/03-incident-response/)) — it teaches any connected agent thefull discovery-and-execute loop without being tied to a specific workflow.
For workflow-specific agent behaviour (custom trigger phrases, UX instructions, step-levelschemas), add a `skill.md` file alongside the workflow. The generic instructions and per-workflowskill files compose cleanly: an agent trained on both knows both the Realm protocol and theworkflow-specific details.
For the full protocol — `next_actions`, `chained_auto_steps`, `context_hint`, and error recovery — see [MCP Protocol Reference](reference/mcp-protocol.md).
---
## 13. Testing Workflows
Write a fixture file in `extraction-demo/fixtures/happy-path.yaml`:
```yamlworkflow: extraction-demodescription: 'Complete happy-path run'params: {}steps: step_one: output: result: 'the document text' finalize: gate_response: approved: trueexpected_final_state: completed```
Run the tests:
```bashrealm workflow test ./ --fixtures ./fixtures/```
For programmatic tests, use `@sensigo/realm-testing`. The `runFixtureTests` runner drives aworkflow to completion for every fixture file in a directory and returns one result per file:
```typescriptimport { describe, it, expect } from 'vitest';import { runFixtureTests } from '@sensigo/realm-testing';import path from 'node:path';import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe('extraction-demo fixtures', async () => { const results = await runFixtureTests({ workflowPath: path.join(__dirname, '../extraction-demo'), fixturesPath: path.join(__dirname, '../extraction-demo/fixtures'), });
for (const result of results) { it(result.name, () => { expect(result.passed, result.error).toBe(true); }); }});```
For the full testing API — `InMemoryStore`, `createAgentDispatcher`, `createGateResponder`,`MockServiceRecorder`, all assertion helpers, `startGitHubMockServer`, and the programmaticstep-by-step walkthrough — see the [Testing Reference](reference/testing.md).
---
---
## Next Steps
- Browse the [`examples/03-incident-response/`](../examples/03-incident-response/workflow.yaml) workflow for a realistic 4-step pattern with a filesystem adapter, two agent steps with personas, and a human gate.- Read the [YAML Schema Reference](reference/yaml-schema.md) for all step fields, execution modes, and DAG dependencies.- Read the [MCP Protocol Reference](reference/mcp-protocol.md) for full tool and response envelope documentation.- Read the [Testing Reference](reference/testing.md) for the full `@sensigo/realm-testing` API.- Read the [`@sensigo/realm` source](../packages/core/src/index.ts) for the full public API.