Core
Teams
A team runs many employees together — a flat parallel fan-out or a coordinator-planned DAG with dependencies, conditional edges, bounded concurrency, and self-heal replan. Define one inline, or save a preset and run it repeatedly.
Run a team inline#
A coordinator plans and delegates to workers; an optional synthesizer folds the results. Guardrails set here are inherited by every member.
const { runId } = await bahini.runSwarm({
coordinatorAgentId: "agent_coord",
workerAgentIds: ["agent_scout", "agent_analyst", "agent_writer"],
synthesizerAgentId: "agent_editor",
prompt: "Produce the weekly BI digest.",
guardrails: { guards: ["pii", "secrets", "injection"], mode: "enforce" },
concurrency: 2, // parallelism cap
swarmMaxTokens: 500_000, // hard cost cap for the whole run
});Wire a DAG#
Pass a plan to shape the workers into a graph. Each node names a worker by key with dependsOn (hard — a failed upstream skips the node) and optionalDependsOn (soft — the node still runs on partial inputs). A condition makes a conditional edge for dynamic branching.
const { runId } = await bahini.runSwarm({
coordinatorAgentId: "agent_coord",
workerAgentIds: ["scout", "analyst", "writer"],
prompt: "Draft the competitor brief.",
plan: [
{ id: "a", employee: "scout" },
{ id: "b", employee: "analyst", dependsOn: ["a"] },
{ id: "c", employee: "writer", dependsOn: ["b"],
condition: { node: "b", contains: "PRICE_CHANGE" } },
],
});validateSwarmPlan — it returns { ok: false, errors } naming unknown employees, duplicate ids, unknown deps, or cycles.Or say it in one line#
For a staged fan-out and fan-in — the common shape — flowis the same graph written the way you'd say it. A comma runs employees together; -> makes the next stage wait for the whole stage before it.
const { runId } = await bahini.runSwarm({
coordinatorAgentId: "agent_coord",
workerAgentIds: ["scout", "pricer", "analyst"],
prompt: "Draft the competitor brief.",
flow: "scout, pricer -> analyst",
});Reach for plan when you need what flow cannot say — soft edges, conditional branches, or a node that repeats. validateSwarmPlan takes either, and gives a flow and the plan it expands to the identical answer. One difference: a malformed plan falls back to a fan-out, but a malformed flow is rejected — you typed it, so you can fix it.
Build a team you don't have yet#
A coordinator can only plan over employees that already exist, so an empty workspace has nothing to coordinate. buildSwarm designs the roster: a coordinator, two to six specialists, a synthesizer, and the DAG wiring them — created as real employees and a real preset you can edit like any other.
const built = await bahini.buildSwarm({
description:
"Every Monday, check our three competitors' pricing pages, flag " +
"anything that moved more than 5%, and write it up for sales.",
});
if (!built.ok) {
console.log(built.clarification); // too vague — ask again, don't retry
} else {
console.log(built.draft.flow); // "price-scout, page-watcher -> change-analyst"
console.log(built.team?.reusedAgentKeys); // employees you already had
await bahini.runPreset(built.team!.presetId, { prompt: "This week's check." });
}dryRun: true to get the draft back without creating anything. An employee you already have under one of the drafted keys is wired in as-is — its instructions are never overwritten.Watch the graph#
getRunDagjoins the persisted plan with each node's live status — poll it (or pair with waitForRun) to render the graph as nodes go running → completed / failed / skipped. Returns null for a non-coordinator run.
const dag = await bahini.getRunDag(runId);
for (const node of dag?.nodes ?? []) {
console.log(node.id, node.agentName, node.status);
}Presets (reusable teams)#
createSwarmPreset(input)
Save a team's shape once, run it repeatedly with runPreset. kind: "basic" runs workers in parallel; "coordinator" adds an orchestrator.
const { id } = await bahini.createSwarmPreset({
key: "weekly-bi",
name: "Weekly BI",
kind: "coordinator",
coordinatorAgentId: "agent_coord",
workerAgentIds: ["scout", "analyst", "writer"],
});
// Persist a guardrail policy so every run of the preset is governed:
await bahini.setPresetGuardrails(id, { guards: ["pii"], mode: "enforce" });
await bahini.runPreset(id, { prompt: "Run this week's digest." });