Assembly LineDocs

Building Agents

Build an agent from scaffold to gated tools, a channel, an automation, and passing evals.

Edit

An Assembly Line agent is a folder. This tutorial builds a complete agent one file at a time, from assembly-line init to passing evals. Each step gives the exact command, the exact file content, and the expected output, then links to the Agent Build Stack page that owns that file's full option surface.

Commands use the installed assembly-line form. In a source checkout, run them as pnpm assembly-line <command> (see Getting Started).

  1. Scaffold the agent
  2. Write the instructions
  3. Add a tool with an approval gate
  4. Add a channel and test it over HTTP
  5. Add an automation
  6. Write two evals and run them
  7. Ship it

1. Scaffold The Agent

assembly-line init agent
Created Assembly Line agent at /path/to/agent
Next: export OPENAI_API_KEY, then: pnpm assembly-line run /path/to/agent --message "hello"

The scaffold is the smallest useful agent:

agent/
  instructions.md   # trusted, always-on guidance
  agent.ts          # identity/policy plus runtime capability hooks
  gateway.ts        # deploy/runtime/state/blob/sandbox/scheduler adapters
  .env.example      # provider env template
  tools/
    echo.ts         # one typed model-callable action

Only instructions.md and agent.ts are required; everything else is optional. Give the agent a name and description in agent.ts with defineAgent:

// agent.ts
import { defineAgent, useModel, useTool } from "@assemblyline-agents/core";

export default defineAgent({
  name: "notes-agent",
  description: "Records durable notes behind an approval gate.",
  setup() {
    useModel("openai/gpt-5.4-mini");
    useTool("echo");
  }
});

The full folder map, including context.ts, skills/, connections/, sandbox/, subagents/, and instrumentation.ts, is in the Agent Build Stack overview. Every defineAgent static policy and built-in hook, including useOutputSchema(), persistent workflow state, maxIterations, selfImprovement, dynamicAutomations, and dynamicConnections, is on agent.ts.

2. Write The Instructions

instructions.md is trusted, always-on guidance. It is the one prose file the model always sees. Replace the scaffold line with an identity and three rules:

You are a concise note-taking agent.

- Use tools when they are available instead of describing what you would do.
- Ask before recording anything durable.
- Keep replies to a few sentences.

Validate after every step:

assembly-line validate agent
Valid Assembly Line agent: /path/to/agent

What belongs in instructions versus skills, tool descriptions, or memory is covered in instructions.md.

3. Add A Tool With An Approval Gate

Each file in tools/ becomes one model-facing tool. The filename is the tool name. The scaffolded tools/echo.ts has no side effects. Add a second tool with a side effect and require approval:

// tools/record_note.ts
import { approvalRequired, defineTool } from "@assemblyline-agents/core";

export default defineTool({
  description: "Record a durable note after explicit approval.",
  inputSchema: {
    type: "object",
    properties: {
      note: { type: "string" }
    },
    required: ["note"]
  },
  needsApproval: approvalRequired("Recording a note is a durable side effect."),
  async execute(input: { note: string }, ctx) {
    await ctx.emit("note.recorded", {
      note: input.note,
      idempotencyKey: ctx.idempotencyKey("record-note")
    });
    return { recorded: true, note: input.note };
  }
});

Enable the compiled tool by adding useTool("record_note") to agent.ts setup(). Tool files define how actions work; hooks decide which actions are available in the current capability snapshot.

Run it without approval to watch the gate pause the run:

assembly-line run agent --tool record_note --input '{"note":"Ship Friday"}'
{
  "run": {
    "id": "3f9d2b1e-…",
    "status": "waiting_for_approval",
    ...
  },
  "waitingForApproval": true,
  ...
}

Approve it and it completes:

assembly-line run agent --tool record_note --input '{"note":"Ship Friday"}' --approve
{
  "run": {
    "id": "8a41c6d0-…",
    "status": "completed",
    ...
  },
  "response": "{\"recorded\":true,\"note\":\"Ship Friday\"}",
  ...
}

Approval policies and side-effect classes, durable steps, sandbox execution, and toModelOutput projections that keep rich results out of model context are on tools/.

4. Add A Channel And Test It Over HTTP

Channels normalize external events into agent turns and deliver replies back to the provider. Add a raw HTTP channel for local development:

// channels/http.ts
import { defineChannel } from "@assemblyline-agents/core";

export default defineChannel({
  description: "Receive a local/dev HTTP message.",
  transport: "http",
  route: "/message",
  methods: ["POST"]
});

A channel turn is a full model turn, so set the provider key for the model in agent.ts, then serve the agent:

export OPENAI_API_KEY=sk-...
assembly-line serve agent --port 3000
Assembly Line runtime serving 4b0c9a17…
http://127.0.0.1:3000

In a second terminal, post to the route:

curl -X POST http://127.0.0.1:3000/message \
  -H "content-type: application/json" \
  -d '{"message":"hello"}'
{
  "runId": "3f9d2b1e-…",
  "status": "completed",
  "response": "Hello! How can I help you today?",
  "waitingForApproval": false,
  "waitingForInput": false,
  "waitingForConnection": false,
  "eventCount": 8,
  "toolCallCount": 0
}

This raw shape is open in dev mode only. In production, generic HTTP channels require host auth. Provider-facing routes should export normalizeHttp() to verify and normalize the provider event before starting a turn. Provider helpers for Slack, Discord, Telegram, Microsoft Teams, and Photon/Spectrum keep webhook wiring in one file. Install them with assembly-line add <channel> agent; see channels/.

5. Add An Automation

Files in automations/ compile into schedule- or event-triggered durable work:

// automations/morning_brief.ts
import { defineAutomation } from "@assemblyline-agents/core";

export default defineAutomation({
  description: "Run a small daily brief.",
  trigger: {
    type: "schedule",
    cron: "0 8 * * *",
    timezone: "America/Chicago"
  },
  idempotencyKey: "notes-agent:morning-brief",
  message: "Summarize yesterday's notes."
});

Rebuild and inspect the compiled schedule table:

assembly-line build agent
Built Assembly Line agent revision 9e5d2c80…
Artifact: /path/to/agent/.assembly-line

.assembly-line/automations.json now lists the automation. Event triggers, trusted lifecycle handlers, and runtime-created dynamic automations are covered in automations/.

6. Write Two Evals And Run Them

evals/*.json is the agent's golden dataset, run through the same compiled runtime path used in production. Start with one normal case and one high-risk case. Both force a tool run, so they are deterministic and need no provider key.

evals/echo_roundtrip.json:

{
  "name": "Echo round trip",
  "input": {
    "message": "ping",
    "tool": "echo",
    "toolInput": { "message": "ping" }
  },
  "expect": {
    "status": "completed",
    "toolsCalled": ["echo"]
  }
}

evals/approval_gate.json:

{
  "name": "Note requires approval",
  "tags": ["high-risk"],
  "input": {
    "message": "Record that we ship Friday.",
    "tool": "record_note",
    "toolInput": { "note": "Ship Friday" },
    "approve": false
  },
  "expect": {
    "status": "waiting_for_approval",
    "toolsCalled": ["record_note"]
  }
}

Run the suite:

assembly-line eval agent
✓ Echo round trip  84ms  $0.000000
✓ Note requires approval  41ms  $0.000000

2/2 executions passed (100.0%); 0 failed; 0 errored
2 unique cases; 1 repetition requested
Cost: runs $0.000000 + judges $0.000000 = $0.000000
Tags:
  high-risk        1/1 passed, 0 failed, 0 errored

Each case gets isolated state, blob, and sandbox roots, and channel senders are never invoked. The full case contract, multi-turn conversations, output and JSON Schema assertions, tool trajectories, tool mocks, state fixtures, custom evaluators, LLM-as-judge, repetitions, baselines, and CI gates, is on evals/.

7. Ship It

gateway.ts declares where the agent runs. The scaffold pins every slot local:

// gateway.ts
import { adapter, defineGateway } from "@assemblyline-agents/core";

export default defineGateway({
  deploy: adapter("local"),
  runtime: adapter("node"),
  state: adapter("local"),
  blob: adapter("local"),
  sandbox: adapter("local"),
  scheduler: adapter("local")
});

Swap slots without touching agent code, for example, assembly-line add postgres agent sets state: adapter("postgres"). Preview the deploy plan without publishing:

assembly-line deploy agent --dry-run

This prints the deploy plan JSON: target, environment, required env, and any missing requirements. Adapter roles and gateway conventions are on gateway.ts; deploy targets, host auth, secrets, and the production checklist are in Runtime And Deployment.

Design Rules

The canonical authoring rules cover file ownership, secrets, and untrusted-context boundaries. They live in the Agent Build Stack overview.

On this page