Skip to content
PricingBlog
✨ Markdown

Worker

A generic, sandboxed subagent type. Workers are spawned by other agents (typically Horton) via the spawn_worker tool — the spawner provides a system prompt and picks the subset of tools the worker should have access to.

Source: packages/agents/src/agents/worker.ts

Spawn args

ts
interface WorkerArgs {
  systemPrompt: string
  tools?: Array<WorkerToolName>
  sharedDb?: { id: string; schema: SharedStateSchemaMap }
  sharedDbToolMode?: "full" | "write-only"
}
FieldRequiredDescription
systemPromptYesThe worker's system prompt. Brief it like a colleague: file paths, line numbers, deliverable.
toolsNoSubset of valid tool names (see below). Unknown names throw at parse time.
sharedDbNoShared state stream id and schema to connect to.
sharedDbToolModeNoShared state tool mode: "full" (default) or "write-only".

registerWorker(registry, { workingDirectory, streamFn? }) is called by the dev server during bootstrap; you don't usually call it yourself.

Valid tool names

ts
type WorkerToolName =
  | "bash"
  | "read"
  | "write"
  | "edit"
  | "brave_search"
  | "fetch_url"
  | "spawn_worker"

These are the same primitives Horton uses. Pick the smallest subset the worker needs — tools are the worker's permission set.

The worker must receive at least one tool or a sharedDb config. If sharedDb is provided, the worker gets generated shared-state tools in addition to any selected primitives.

Spawning a worker

The canonical way to spawn a worker is the spawn_worker tool, which Horton calls from inside its agent loop:

ts
spawn_worker({
  systemPrompt:
    "You are a focused researcher. Find the three most-cited papers on X and return their titles, authors, and DOIs as a markdown table.",
  tools: ["brave_search", "fetch_url"],
  initialMessage: "Begin research now.",
})
FieldRequiredNotes
systemPromptYesSets persona and constraints.
toolsYesSubset of WorkerToolName. Must contain at least one entry.
initialMessageYesFirst user message. Without this the worker idles — nothing kicks it off.

The spawn uses wake: { on: 'runFinished', includeResponse: true }, so the spawner wakes when the worker finishes its run and receives the worker's response in the wake message.

What the handler does

  1. Parses ctx.args into WorkerArgs. Throws if systemPrompt is empty, if tools contains an unknown name, or if neither tools nor sharedDb is provided.
  2. Builds the requested tool instances against the worker's workingDirectory (and a fresh per-wake readSet for the read-first-then-edit guard).
  3. If sharedDb is present, connects with ctx.observe(db(id, schema)) and exposes generated read_*, write_*, update_*, and delete_* tools (write_* only in "write-only" mode).
  4. Configures the agent with HORTON_MODEL (claude-sonnet-4-5-20250929), the provided system prompt (with a brief reporting-back footer appended), and the assembled tool list.
  5. Runs the agent until the LLM stops.

Least-privilege sandbox

Workers deliberately do not receive ctx.electricTools. The spawner already picked the worker's tool subset; granting entity-runtime primitives (cron, schedule, send-to-arbitrary-entity) would let a worker escape that scope. If a worker needs those primitives, it must spawn its own subagent or report back to the spawner.

The runtime appends this to every worker's system prompt:

text
# Reporting back
When you finish, respond with a concise report covering what was done and any key findings. The caller will relay this to the user, so it only needs the essentials.

Details

PropertyValue
Type nameworker
ModelHORTON_MODEL (claude-sonnet-4-5-20250929)
ToolsSubset of 7 primitives plus optional shared-state tools. No ctx.electricTools.
Working directoryProvided to registerWorker at bootstrap
DescriptionInternal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).