Skip to main content

Agents

An agent is a named, reusable participant you define once and use many times. It bundles three things: a persona (the system-prompt identity that says who the agent is and how it behaves), a model, and a set of tools it may call. Once registered, you reference an agent by name from a pattern skill's worker map, or drop it into any flow action as a standalone block.

The point is to stop copy-pasting the same prompt and tool list into every worker. You write the research-analyst once, then a supervisor skill, a plan-and-execute skill, and a one-off action can all point at it.

Agents live in @flow-state-dev/workforce. The type contracts they satisfy are declared in @flow-state-dev/core, so other packages can accept an agent registry without depending on workforce.

defineAgent

defineAgent builds a validated agent definition. The config:

FieldMeaning
nameStable identifier. This is the key agent-ref resolves against.
descriptionRouting-facing summary shown in trace UI and the DevTool. Not the system prompt.
personaThe system-prompt source. A string, an inline template, or a resource path.
modelModel id. Falls back to the deps' default, then intent/chat.
allowedToolsTool-catalog keys the agent may reference.
usesCapabilitiesCapabilities the agent composes, as string keys or capability refs.
outputSchemaStructured output contract. Honored only for the standalone shape.
itemVisibilityWhich items reach the client and history. Defaults to { client: true, history: false }.

A minimal agent needs a name, a description, a persona, and whatever tools it calls:

import { defineAgent, createAgentRegistry } from "@flow-state-dev/workforce";

const techBriefer = defineAgent({
name: "tech-briefer",
description: "Produces concise technology briefings from web research.",
persona:
"You are a senior technology analyst at a research firm. " +
"Write concise, opinionated briefings. Lead with the takeaway, " +
"then supporting evidence, then risks. Cite every claim. " +
"If sources conflict, show the conflict rather than picking a side.",
model: "openai/gpt-5.4-mini",
allowedTools: ["search", "fetch"],
});

export const agentRegistry = createAgentRegistry([techBriefer]);

Registry and materialization

An agent definition is inert on its own. Two steps turn it into something that runs.

createAgentRegistry builds a catalog from an array of agents. It errors on a duplicate name, so the registry is a single source of truth. materializeAgent turns an agent into a runnable block, either worker-shaped (for a pattern skill) or standalone (for a flow action).

You rarely call materializeAgent by hand. You hand it to the skills capability, which calls it when a pattern skill's worker resolves an agent-ref:

import { createAgentRegistry, materializeAgent } from "@flow-state-dev/workforce";
import { createSkillsCapability } from "@flow-state-dev/orchestration";
import { defaultPatternRegistry } from "@flow-state-dev/patterns";

const skillsCap = createSkillsCapability({
catalog,
agentRegistry,
materializeAgent,
patternRegistry: defaultPatternRegistry,
});

With that wiring in place, a pattern skill's SKILL.md references an agent from its workers: map. agent-ref names the agent; agent-overrides adjusts it for this skill:

---
description: Multi-angle research on a company. Use when the user asks for a deep dive.
pattern: task-board

workers:
analyst:
agent-ref: tech-briefer
agent-overrides:
model: openai/gpt-5.4-mini
tools: [search, fetch, readDocument]

initial-tasks:
- id: market
goal: Analyze the market positioning of $ARGUMENTS
assignee: analyst
---

Overrides use REPLACE semantics, not merge. If agent-overrides.tools is present, it replaces the agent's allowedTools entirely; the two lists are not combined. Same for model and itemVisibility. The result is a deterministic, auditable tool surface per skill: you read the override block and know exactly what the worker can do.

There's no prompt or persona override. Changing an agent's persona is a change to the agent definition. If you need an ad-hoc prompt for one worker, use prompt or prompt-ref on the worker spec instead of agent-ref.

Standalone block

agentBlock composes an agent directly into a flow action, no skill involved. It's a shorthand around the standalone materialization:

import { agentBlock } from "@flow-state-dev/workforce";

const briefingBlock = agentBlock(techBriefer, { catalog });
// Input: { goal: string }, Output: string

Mount briefingBlock in a sequencer or wire it as an action the same way you would any block.

Structured output and capabilities

A standalone agent can return typed data instead of free text. Declare an outputSchema and the materialized generator emits that shape, subject to the same OpenAI-strict requirement as any generator output. Workers stay z.string() regardless, because the pattern machinery builds follow-on actions from a worker's text.

usesCapabilities accepts two forms in the same array: a string key resolved against the materialize-time capability catalog, or a capability reference used as-is. A reference can be configured with .with({ ... }), and the preset typing carries through, the same way generator({ uses }) consumes capabilities.

import { defineAgent } from "@flow-state-dev/workforce";
import { tradingDeskCapability } from "./capabilities/trading-desk";
import { z } from "zod";

const positionSizerSchema = z.object({
ticker: z.string(),
action: z.enum(["buy", "sell", "hold"]),
sizePct: z.number(),
});

const positionSizer = defineAgent({
name: "position-sizer",
description: "Sizes a position into a typed decision.",
persona: { path: "personas/portfolio-manager" },
outputSchema: positionSizerSchema, // standalone only
usesCapabilities: [
tradingDeskCapability.with({ valuationSpine: true }), // typed capability ref
"marketDataAccess", // string key, resolved from the catalog
],
});

Personas

A persona is the agent's identity, the system prompt. It can come from three sources.

FormDescription
stringBare system prompt, used verbatim. The simplest form.
{ template, state? }Inline LiquidJS template rendered against optional state.
{ path }A declared resource or collection instance, rendered live via readContent().

For resource-backed personas, definePersona declares them the same way skills are declared, as a collection over a path pattern with a content template:

import { definePersona } from "@flow-state-dev/workforce";

const personas = definePersona({
pattern: "personas/*",
contentTemplate: "You are a {{ state.role }}. {{ state.instructions }}",
});

An agent then sources its persona by path (persona: { path: "personas/portfolio-manager" }), and the content resolves live at execution time. A missing path or empty content surfaces as an execution-time error, not a definition-time one.

Current limits

Agents are a young primitive. A few things are declared but not yet wired, and it's worth knowing before you lean on them:

  • usesSkills is reserved. It's on the type, but nothing resolves it yet.
  • contextMode: "fork" is not honored. Only "inline" runs today; a "fork" value is accepted but treated as inline.
  • Agents are registered statically at build time through createAgentRegistry. There's no runtime registration.
  • There's no cross-flow agent assignment and no Roles or Workstreams layer. An agent is referenced within a flow, by a skill or a standalone block, not assigned across flows.