SDK Reference
Install the SDK:
npm install @boboddy/sdkbun add @boboddy/sdkdefineStep(options)
Section titled “defineStep(options)”Define a reusable, versioned step with typed input/output schemas.
import { defineStep } from "@boboddy/sdk";import { z } from "zod";
const myStep = defineStep({ key: "my-step", name: "My Step", version: 1, description: "Does something useful.", additionalInput: z.object({ text: z.string() }), result: z.object({ summary: z.string(), score: z.number() }), signals: [ { sourcePath: "score", key: "quality_score", type: "number", required: true, }, ], agentPrompt: ({ input, env, boboddy }) => `Analyze the provided text from ${input.text}.Base URL: ${env.BASE_URL}Write any generated files to ${boboddy.artifactsDir}Return a summary and quality score.`, status: "active",});StepDefinition options
Section titled “StepDefinition options”| Field | Type | Required | Description |
|---|---|---|---|
key |
string |
Yes | Unique step key within the project |
name |
string |
Yes | Display name |
version |
number |
No | Version (default: 1) |
description |
string |
No | Short description |
agentPrompt |
string | ((context) => string) |
Yes | AI instruction given to the executing agent |
additionalInput |
ZodType |
No | Additional input payload schema; fields are bound via the pipeline mapper |
result |
ZodType |
No | Output payload schema |
signals |
Signal[] |
No | Values to extract from the result |
mcpServers |
OpenCodeMcpServers |
No | MCP server configs for tool-using agents |
plugins |
OpenCodePluginEntry[] |
No | Opencode plugins merged into the step’s execution config |
features |
StepFeature[] |
No | Built-in feature plugins (e.g. Features.notifications()) |
status |
"draft" | "active" |
No | Draft steps are skipped by workers |
executionMode |
"workspace" | "no_workspace" |
No | "no_workspace" runs the agent without cloning your repo or a dev container; defaults to "workspace". See Execution mode |
agentPrompt
Section titled “agentPrompt”agentPrompt accepts either a raw string or a function that receives a typed prompt context. The function form is recommended because it gives autocomplete for supported prompt variables and keeps prompt tokens consistent with your step schema.
const browserReproStep = defineStep({ key: "browser-repro", name: "Browser Repro", additionalInput: z.object({ title: z.string(), description: z.string(), }), agentPrompt: ({ input, env, boboddy }) => `Open ${env.BASE_URL}.Reproduce the issue described in ${input.title}.Save traces to ${boboddy.artifactsDir}trace.zip.`,});Prompt context scopes
Section titled “Prompt context scopes”| Scope | Example | Source |
|---|---|---|
input |
${input.title} |
Step execution input bound through the pipeline |
env |
${env.BASE_URL} |
Any defined environment variable available to the worker |
boboddy |
${boboddy.artifactsDir} |
Boboddy-provided runtime values |
At runtime these become {{input.title}}, {{env.BASE_URL}}, and {{boboddy.artifactsDir}} inside the stored prompt template.
Boboddy currently provides:
boboddy.artifactsDirfor files that should be uploaded as step artifacts.
Legacy raw prompt tokens such as {{title}} and {{stepArtifactsDir}} still resolve, but new steps should prefer the scoped form.
Signal
Section titled “Signal”type Signal = { sourcePath: string; // dot-notation path into result, e.g. "metrics.score" key?: string; // signal name used in advancement rules (defaults to sourcePath) type?: "number" | "string" | "boolean" | "object" | "array"; required?: boolean; // fail execution if signal is missing};mcpServers
Section titled “mcpServers”mcpServers is a Record<string, McpServerConfig> where each value is one of three shapes:
// Local subprocess server. `command` is [executable, ...args] — no separate `args` field.type LocalMcpServer = { type: "local"; command: string[]; environment?: Record<string, string>; // {env:VAR} interpolates a worker env var enabled?: boolean; timeout?: number;};
// Remote HTTP server.type RemoteMcpServer = { type: "remote"; url: string; headers?: Record<string, string>; oauth?: | { clientId?: string; clientSecret?: string; scope?: string; redirectUri?: string } | false; enabled?: boolean; timeout?: number;};
// Enabled override — toggle an inherited server without redefining it.type McpEnabledOverride = { enabled: boolean };See MCP servers for examples, and
Secrets for how {env:VAR} resolves and
where the real values live.
plugins
Section titled “plugins”plugins is an array of Opencode plugin entries merged into the step’s execution config:
type OpenCodePluginEntry = string | [packageName: string, options: Record<string, unknown>];Entries are deduplicated by package name when combined with the baseline Opencode config.
Features
Section titled “Features”features accepts built-in feature plugins from the Features namespace (imported from @boboddy/sdk/definitions/steps). A feature extends the step’s result schema, appends signals, and injects prompt text.
import { defineStep, Features } from "@boboddy/sdk/definitions/steps";
const step = defineStep({ key: "repro", name: "Repro", agentPrompt: "Reproduce the bug.", features: [Features.notifications()],});| Feature | Effect |
|---|---|
Features.notifications() |
Adds a $boboddy_notifications_v1 result array + signal (type array, not required) and prompt text for emitting user notifications. |
Features.feedbackRequests() |
Convenience wrapper over notifications(), backed by the same $boboddy_notifications_v1 signal, aimed at asking the project team questions. |
Each notification item is:
type NotificationItem = { kind: "feedback_request" | "status_update" | "blocked" | "result_ready" | "warning"; title: string; body: string; priority: "low" | "normal" | "high" | "urgent"; suggestedChannels?: ("in_app" | "work_item_platform_comment" | "email" | "slack")[]; payload?: Record<string, unknown>; // feedback_request: { category, urgency, suggestedKey? }};Features.notifications.signal.find(signals) and Features.notifications.signal.key are helpers for reading emitted notifications from a step’s signals.
See Features for the guide.
pipeline(meta)
Section titled “pipeline(meta)”Define an ordered sequence of steps using the fluent builder.
import { pipeline } from "@boboddy/sdk/definitions/pipelines";import { z } from "zod";
const inputSchema = z.object({ text: z.string() });
const myPipeline = pipeline({ key: "my-pipeline", name: "My Pipeline", status: "active", additionalPipelineInput: { schema: z.object({ text: z.string() }), bindings: ({ workItem }) => ({ text: workItem.field("Text") }), },}) .step(myStep, { input: ({ input }) => ({ text: input.text }), advance: () => ({ default: "continue" }), }) .build();PipelineMeta options
Section titled “PipelineMeta options”| Field | Type | Required | Description |
|---|---|---|---|
key |
string |
Yes | Unique pipeline key |
name |
string |
Yes | Display name |
version |
number |
No | Version (default: 1) |
description |
string |
No | Short description |
status |
"draft" | "active" |
No | Draft pipelines are not executed |
additionalPipelineInput |
object |
No | Custom input fields; requires both schema and bindings |
additionalStepInput |
object |
No | Default bindings applied to every step in the pipeline |
additionalPipelineInput.schema is a Zod object schema for extra pipeline input fields. additionalPipelineInput.bindings receives { workItem, literal } and returns their bindings.
additionalStepInput applies default bindings to every step in the pipeline. Its bindings function receives { workItemField, literal } and compiles into regular step input bindings. Explicit options.input bindings on a .step() call override pipeline-level defaults.
Builder methods
Section titled “Builder methods”| Method | Description |
|---|---|
.step(step, options) |
Append a step. options.input receives { input, signal, output, literal, signalsList } and returns a record of input bindings keyed by the step’s input fields (optional if the step declares no additional input). options.advance is required — receives { signal, stepSignals, all, any, route, avg, sum, min, max, count, weightedAvg, booleanAny, booleanAll } and returns { default, rules? }, deciding how the pipeline continues past this step. options.timeout (optional, seconds) caps execution time. |
.fanOutStep(step, config) |
Runs step as N parallel branches instead of one, in place of .step(...). config resolves both the per-branch and whole-cohort advancement policies inline (advance, advanceAll) alongside over, input, and timeout — see Fan-out for FanOutStepConfig’s full field list. |
.build() |
Finalize and return a PipelineDefinitionSpec. |
Step input bindings
Section titled “Step input bindings”Inside .step()’s input option:
input.workItemTitle/input.workItemDescription— always available; bind to the work item title or description.input.<path>— custom fields fromadditionalPipelineInput.schema.input.codebinds to path"code";input.ticket.titlebinds to"ticket.title". The accessor is a proxy — do not spread or coerce it to a primitive.signal(step, signalKey)— bind to a prior step’s signal.signalKeyis typed againststep.__signalKeys.output(step)— bind to a prior step’s whole output object.literal(value)— a hardcoded constant.signalsList(fanOutStep)— bind to a fan-out’s whole cohort (every terminal branch’s signals, aggregated server-side).fanOutStepis constrained to a step already passed as the first argument to an earlier.fanOutStep(...)call. See Fan-out.
Fluent advancement rules
Section titled “Fluent advancement rules”Inside .step()’s advance option:
signal(key)— returns a typedSignalReffor the current step’s signal. Chain a comparator (.eq,.gt,.gte,.lt,.lte,.ne,.in,.notIn,.contains,.doesNotContain) followed by.then(outcome).stepSignals.<key>— property-map shorthand equivalent tosignal(key). Both produce identical output.- Computed factories —
avg,weightedAvg,sum,min,max,count,booleanAny,booleanAll. Each takes 2+signal(key)orstepSignals.keyreferences and returns aSignalRef. Identical calls across rules are deduplicated at build time. all(...refs)/any(...refs)— groupSignalRefs and other groups; terminate with.then(outcome).route(pipelineKey, inputJson?)— produces a route outcome value for.then(...).
Fan-out
Section titled “Fan-out”.fanOutStep(step, config) runs step as a variable number of parallel branches, resolved at runtime from a signal’s value. It always compiles to a paired fanOut + cohortGate node; config resolves both the per-branch (advance) and whole-cohort (advanceAll) advancement policies inline, the same way .step()’s advance option resolves inline rather than via a chained call. See the Fan-out guide for a full example.
.fanOutStep(reviewStep, { over: "reviewer_count", input: ({ literal }) => ({ mode: literal("thorough") }), timeout: 900, advance: ({ signal }) => ({ default: "continue", rules: [signal("passed").eq(false).then("block")], }), advanceAll: ({ branchOutcomes }) => ({ default: "block", rules: [branchOutcomes.every("continue").then("continue")], }),})over’s resolved signal shape decides the branch mode: a number-typed signal resolves a fixed branch count (no item on input’s ctx); an array-typed signal resolves branch count from the array’s length and adds a typed item — the array’s element type — to every branch’s input mapper.
.fanOutStep(assigneeReviewStep, { over: "assigneeIds", // a string[] signal on the preceding step input: ({ item, input }) => ({ assigneeId: item, ticketTitle: input.workItemTitle, }), advance: () => ({ default: "continue" }), advanceAll: () => ({ default: "continue" }),})FanOutStepConfig
Section titled “FanOutStepConfig”| Field | Type | Required | Description |
|---|---|---|---|
over |
string |
Yes | Signal on the most recently declared step whose value determines branch count (and, if array-typed, each branch’s item) at runtime |
advance |
(ctx) => result |
Yes | Every branch’s own continue/block decision — see advance context below |
advanceAll |
(ctx) => result |
Yes | The whole cohort’s continue/block decision — see advanceAll context below |
input |
(ctx) => bindings |
No | Input bindings for the fan-out step; same context helpers as .step(...)’s mapper, plus item when over resolves to an array-typed signal |
timeout |
number | null |
No | Per-branch timeout in seconds |
advance context
Section titled “advance context”Evaluated per-branch against that branch’s own signals:
signal(key)/stepSignals.<key>— the branch’s own signal, typed againstfanOutStep.__signalKeys. Same comparators as a regular step’sSignalRef(.eq,.gt,.gte,.lt,.lte,.ne,.in,.notIn,.contains,.doesNotContain).all(...refs)/any(...refs)— group refs; terminate with.then(outcome)..then(outcome)—outcomeis restricted to"continue" | "block"(noroute/complete).- No computed-signal factories (
avg,sum, etc.) — core has no mechanism yet to resolve a computed signal for a fan-out branch’s policy.
advanceAll context
Section titled “advanceAll context”Evaluated once per cohort, after every branch has settled:
branchOutcomes.total()— the cohort’s total branch count (factbranchCount). Returns aCohortSignalRef<number>.branchOutcomes.count(outcome)— a single outcome’s count across the cohort (fact${outcome}Count). Returns aCohortSignalRef<number>.branchOutcomes.every(outcome)/.some(outcome)—trueiff every/any branch resolved tooutcome. Already aCohortRuleLeaf— go straight to.then(outcome).stepSignalsList.pluck(signalKey)— seeds an aggregation across every branch’ssignalKeyvalue. Chain.filter(operator, value)/.sortBy(direction?)/.unique()to reshape, then exactly one reducer:.count(),.sum(),.avg(),.min(),.max(),.booleanAll(),.booleanAny(),.join(separator?),.first(), or.last(). Returns aCohortSignalRef.all(...refs)/any(...refs)— group refs; terminate with.then(outcome).outcomein.then(outcome)is restricted to"continue" | "block".
BranchOutcome (the outcome param to branchOutcomes.count/.every/.some) is "continue" | "block" | "error" | "abandoned" — the two additional values cover branches that failed or never ran.
defaultPipelineAssignment(callback)
Section titled “defaultPipelineAssignment(callback)”Define which pipeline is automatically started when a work item arrives. This goes in the reserved file .boboddy/pipeline-builder/default-pipeline-assignment.ts.
import { defaultPipelineAssignment } from "@boboddy/sdk/definitions/pipelines";import bugTriage from "./bug-triage";import regressionReview from "./regression-review";
export default defaultPipelineAssignment(({ workItem, any, assign, skip }) => ({ default: assign(bugTriage), rules: [ any( workItem.field("status").eq("resolved"), workItem.field("status").eq("manual support"), ).then(skip()), workItem.field("issueType").eq("bug").then(assign(bugTriage)), workItem .field("labels") .contains("regression") .then(assign(regressionReview)), ],}));The callback receives a context object. Only defaultPipelineAssignment needs to be imported.
Callback context
Section titled “Callback context”| Property | Description |
|---|---|
workItem.field(name) |
Returns a comparator ref for the named work item field |
context.isNew |
Comparator ref; true when the work item is new |
assign(pipeline) |
Outcome: start the given pipeline (PipelineDefinitionSpec) |
skip() |
Outcome: do not assign any pipeline |
all(...refs) |
All nested conditions must match |
any(...refs) |
Any nested condition must match |
Return value shape
Section titled “Return value shape”| Field | Type | Description |
|---|---|---|
default |
AssignOutcome | SkipOutcome |
Outcome when no rule matches; assign(pipeline) or skip() |
rules |
AssignmentRule[] |
Ordered rules; first match wins |
Comparators
Section titled “Comparators”All comparators available on advancement SignalRefs are also available here: .eq, .ne, .gt, .gte, .lt, .lte, .in, .notIn, .contains, .doesNotContain.
API client
Section titled “API client”The SDK ships an auto-generated API client built from the OpenAPI spec.
import { createBoboddyClient } from "@boboddy/sdk";
const client = createBoboddyClient("https://app.boboddy.dev");Use createStepDefinitionsClient for CRUD operations on step definitions:
import { createStepDefinitionsClient } from "@boboddy/sdk";
const stepClient = createStepDefinitionsClient("https://app.boboddy.dev");Config helpers
Section titled “Config helpers”JSONC parser
Section titled “JSONC parser”Parse .boboddy/boboddy.jsonc files (JSON with comments):
import { parseJsonc } from "@boboddy/sdk";
const config = parseJsonc(rawString);Project config
Section titled “Project config”Read the Boboddy project config from disk. readProjectConfig is exported from @boboddy/worker (not the SDK):
import { readProjectConfig } from "@boboddy/worker";
const { projectId } = await readProjectConfig();