Working with a coding agent¶
A pipeline is a document and every dg command answers in records, which is most of what a
coding agent -- Claude Code, Codex, OpenCode, or any model with a shell -- needs to write
pipelines and check its own work. This page is what to hand it, and what it will find.
Start it in a project¶
dg init writes AGENTS.md beside the project's README.md: the loop below, how to read what
dg answers, and the rules a document most often gets wrong. CLAUDE.md holds one line,
@AGENTS.md, so Claude Code reads the same notes every other agent reads. A project that
already has either file keeps its own.
A project made before dg init wrote them gets the notes with uv run dg init --agents, which
writes the two files alone and leaves everything else, and any of the two the project already
has, as it is.
Start the agent in that directory and say what you want, in your own words:
Write a pipeline that fetches today's readings from the playground, keeps the stations
above 30 degrees, and writes them to storage as CSV. Check it with dg before you stop.
The loop it works in¶
Every command goes through uv run dg, the version the project pins, and none of it needs a
server.
| Step | Command | What it gives the agent |
|---|---|---|
| Find a starting point | uv run dg examples list --local, uv run dg examples show CODE --local |
Every document the installed plugins ship, each one run by dirigent's own tests |
| Copy one | uv run dg pipeline new STARTER |
The starter in pipelines/, its teaching comments intact |
| Look up a block | uv run dg blocks list --local, uv run dg blocks show BLOCK --local |
Every block's config and output schemas |
| Get the whole shape | uv run dg pipeline schema -f .dirigent/document.schema.json |
One JSON Schema: the format and every installed block's config |
| Check it | uv run dg validate pipelines/NAME.yaml --explain |
Every problem at once, each with the location it is about, and what the work will cost |
| Run it | uv run dg run --local pipelines/NAME.yaml -v |
A throwaway instance, every step's state and output, and the closing run record |
dg dev and dg apply come after: dg dev starts a local instance with its web UI and keeps
running, so start it yourself in a terminal of its own, and the agent applies to it with
uv run dg apply.
Why the answers are easy to read¶
An agent's shell is never a terminal, so every dg command writes NDJSON there, with no
colour, no table and no banner: one JSON record per line, each with a kind. The same command
at your terminal renders, and dg format renders a stream that was kept. See
records when nothing is watching.
A document that does not validate is a validation record whose problems[] each carry a
stable code, a message and the location in the document it is about, all of them at once:
{"kind": "validation", "level": "error", "message": "invalid", "code": "hot-stations", "problems": [
{"code": "document.step_config_invalid", "location": "steps.fetch.config",
"message": "Additional properties are not allowed ('urll' was unexpected)"}
]}
Anything else refused -- by the CLI, by a block's own check, by the server -- is a record of
kind error with a code, the params it was built from, and problems[] where there is
more than one thing to say. A model fixes what location names and validates again, which
converges in a step or two rather than by guessing at prose.
Offline, dg validate checks every step's block and config against the blocks installed
beside dg. A connection or another pipeline a document names is checked only by
dg validate --server or the apply, since only an instance knows what it holds.
An editor, or a model, that checks as it writes¶
dg pipeline schema -f .dirigent/document.schema.json writes the format and every installed
block's config as one Draft 2020-12 schema. Name it on a document's first line and any editor
that reads the comment squiggles an unknown key or a missing one where it was typed:
# yaml-language-server: $schema=../.dirigent/document.schema.json
format: dirigent/v1
kind: pipeline
code: hot-stations
Write it again after installing a plugin; the schema is the blocks installed beside dg. A
running instance serves the same schema for its own catalog at GET /api/v1/schema/document.
Over MCP, against a running instance¶
The loop above writes documents and checks them without a server. To let an agent operate an
instance that is running -- apply a document, start a run, read its logs, pause a source --
give it the MCP server, dirigent-mcp. Its tools are calls the API already answers, so what
the agent may do is what its token's role may do.
No tool is a delete, but apply_document retires what a document stops declaring: a source, with
its input journal and its token, and a schedule the document managed. Runs and pipeline versions
stay. plan_document lists what would go under sources.removed and
triggers.schedules_removed, and apply_document carries the destructive hint, so a client that
asks before such a call asks here.
dg admin token create agent --user AGENT_USER # the token is shown once
claude mcp add dirigent -e DG_URL=https://dirigent.example.org -e DG_TOKEN=... -- uvx dirigent-mcp
Any MCP client starts it the same way: the command uvx dirigent-mcp over stdio, with
DG_URL and DG_TOKEN in its environment. Keep the token where your other secrets are.
What applying and running reaches. An agent that may apply a document and run it may write a pipeline that runs with any connection the instance holds and sends what it reads anywhere an enabled block reaches. Text it reads back -- a log line, a source's input -- can try to steer it there. So choose the role for what the agent is for:
- To read and diagnose, give it a
viewertoken and setDG_MCP_READ_ONLY=1. The instance refuses every write from a viewer, and the server offers only the tools that read. - To write and run, give it an
operatortoken, neveradmin, on an instance whose connections and enabled blocks you would let it use; a staging instance is the usual place.
The server reaches the instance and nothing else on its host. The document of plan_document
and apply_document is always the document's text: a string naming a local file is refused as
text that is not a document, and the file is not opened.
| Tools | What they do |
|---|---|
list_blocks, get_block, list_examples, get_example |
The catalog and the shipped examples, to write a document from |
plan_document, apply_document |
Check a document against the instance, then store it |
list_pipelines, get_pipeline, export_pipeline, validate_pipeline |
What the instance holds |
run_pipeline, list_runs, get_run, run_attempts, run_logs |
Start a run, optionally waiting for it, and read how it went |
run_stats |
How the runs of a window went: per day, each pipeline's p50, p95 and max duration, and what failed most |
cancel_run, retry_attempt, rerun_run |
Stop a run, retry one attempt, or run a settled run again with its parameters and source input |
list_sources, source_inputs, pause_source, resume_source |
The input sources and what arrived |
list_schedules, pause_schedule, resume_schedule |
The clocks |
instance_info |
The instance's version and what it has installed |
Giving it the documentation¶
llms.txt lists every page of this site with a line on what it covers, in the format language models look for at a site's root. Point an agent at it when the question is bigger than a document, or paste the pages it names.
What to keep from it¶
- Secrets. A connection holds them, created once with
dg connection create, which prompts at a terminal; a document names a connection by its code and never carries a secret. Create connections yourself. - Blocks that run code on a worker.
shell.run,docker.*and their kind are refused until the instance allowlists them inenabled_unsafe_blocks. That allowlist is yours to set. - The instance's state.
.dirigent/state/and.envbelong to the local instance.