SDK Reference
You don’t install the SDK yourself to write pipelines. @boboddy/sdk is a
dependency of .boboddy/pipeline-builder/package.json, which
boboddy pipelines design
scaffolds and installs for you (boboddy pipelines init
scaffolds it; you run the install). Add it to another package directly only
to script against the API client.
See the Pipeline Graph catalog entry for an interactive view of a compiled pipeline’s node/edge graph.
defineStep(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 |
healthChecks |
HealthCheck[] |
No | Real tool calls forced against the environment before the agent is prompted. See Health checks |
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 |
environment |
{ runtime?: RuntimeSpec; vars?: ({ input }) => Record<string, string | EnvEntry>; repo?: RepoSpec | (({ input, result }) => RepoSpec) } |
No | Where the step runs, which environment variables it gets, and what it does to your repository; declare before agentPrompt. See Runtime, Env and Repo |
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} |
The worker’s environment (.boboddy/.env over process.env); with declared environment.vars, only its non-secret keys |
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.
Runtime
Section titled “Runtime”Runtime builds the environment.runtime value: where the step runs. It is exported from @boboddy/sdk. Omit runtime for the default: Runtime.devcontainer() on defineStep(), Runtime.managed.bun1() on codeStep().
import { defineStep, Runtime } from "@boboddy/sdk";
defineStep({ key: "build-frontend", name: "Build Frontend", environment: { runtime: Runtime.devcontainer({ config: ".devcontainer/frontend/devcontainer.json", }), }, agentPrompt: "Build the frontend.",});
defineStep({ key: "summarize", name: "Summarize", environment: { runtime: Runtime.host() }, agentPrompt: "Summarize the provided text.",});type RuntimeSpec = DevcontainerRuntimeSpec | HostRuntimeSpec | ManagedRuntimeSpec;
Runtime.devcontainer(opts?: { config?: string; // full repo-relative path to devcontainer.json or .devcontainer.json}): DevcontainerRuntimeSpec;
Runtime.host(): HostRuntimeSpec;| Constructor | Behaviour |
|---|---|
Runtime.devcontainer() |
Clones the repository and runs the step in its devcontainer. Without config, the worker uses .devcontainer/devcontainer.json, then devcontainer.json. |
Runtime.host() |
No clone, no container: the step runs on the worker host in an empty temporary directory. Takes no options. |
config rules, enforced by Runtime.devcontainer (which throws), by definition validation at push, by the server (422), and by the worker:
- The base name must be
devcontainer.jsonor.devcontainer.json. - Repo-relative: no leading
/, no drive letter, no backslashes, no..segment. A leading./is stripped. At most 255 characters. - There is no fallback search. If the file is missing on the branch the step is based on, the step fails with
Devcontainer config "<path>" not found in the cloned repository (branch "<branch>"). The work branch of the nearest earlier step that produced one can change the config a later step finds (see Base branch).
codeStep() accepts the same environment, but Runtime.host() is a type error there and is rejected by validation: a code step runs repo code and needs a workspace. The container a code step’s config selects must provide a JavaScript runtime for the runner script.
Runtime.managed
Section titled “Runtime.managed”codeStep() also accepts Runtime.managed.<id>(): a container the worker supplies (image, config and dependency install), so the step does not boot your project’s devcontainer. defineStep() rejects it. It is the default for codeStep(): omit runtime and you get Runtime.managed.bun1(). Use Runtime.devcontainer() to run a code step in your project’s devcontainer instead.
import { codeStep, Runtime } from "@boboddy/sdk/definitions/steps";
codeStep({ key: "lookup-criteria", name: "Lookup Criteria", environment: { runtime: Runtime.managed.node24() }, fn: lookupCriteriaFn,});type ManagedRuntimeId = "bun1" | "node24";
Runtime.managed: { [K in ManagedRuntimeId]: () => ManagedRuntimeSpec<K> };| Constructor | Image | Behaviour |
|---|---|---|
Runtime.managed.bun1() |
oven/bun:1.4.0-debian |
The default for codeStep(). Runs bun install --production in .boboddy/pipeline-builder, then runs the step with Bun. Skips dependency postinstall scripts unless listed in trustedDependencies. |
Runtime.managed.node24() |
node:24-bookworm-slim |
Runs npm install --omit=dev (or corepack pnpm install --prod for pnpm-lock.yaml), then runs the step with Node 24. Runs postinstall scripts. Does not resolve extensionless relative imports. |
Runtime.managed.nope is a type error: the identifiers come from MANAGED_RUNTIME_IDS in @boboddy/sdk/managed-runtimes. A managed step has no config. See Runtime for code steps for the lockfile handling and runtime differences, and Running Workers for the worker side.
The wire form is derived from runtime: executionMode ("workspace" for the devcontainer and managed runtimes, "no_workspace" for host), devcontainerConfigPath (config, or null), and managedRuntime (the identifier for a managed runtime, otherwise null). The path schema is devcontainerConfigPathSchema from @boboddy/sdk/devcontainer-config-path.
See Environment and Multiple configs for the guides.
environment.vars is a function of { input }, run once at definition time, returning a record of variable name to an EnvEntry. Declare environment before agentPrompt so the keys are typed into the prompt’s env, which is then strict (declared, non-secret keys only). Env is exported from @boboddy/sdk.
import { defineStep, Env } from "@boboddy/sdk";import { z } from "zod";
defineStep({ key: "account-investigation", name: "Account Investigation", additionalInput: z.object({ tenant: z.string() }), environment: { vars: ({ input }) => ({ WAREHOUSE_URL: "https://warehouse.internal", TENANT_API_KEY: Env.value({ value: `${input.tenant}-key`, secret: true }), WAREHOUSE_TOKEN: Env.inherit({ secret: true }), LOG_LEVEL: Env.inherit({ optional: true, default: "info" }), }), }, agentPrompt: ({ env }) => `Query ${env.WAREHOUSE_URL}.`,});type EnvEntry = string | EnvValue | EnvInherit;
Env.value(opts: { value: string; // literal or {{input.…}} template; stored in plaintext secret?: boolean; // mask in logs; requires an {{input.…}} token in `value` unsafeAllowStatic?: boolean; // allow a static value on a credential-looking name; never serialized}): EnvValue;
Env.inherit(opts?: { from?: string; // worker variable to read; defaults to the key secret?: boolean; // mask in logs; cannot be combined with `default` optional?: boolean; // omit instead of failing when the worker has no value default?: string; // used when the worker has no value; stored in plaintext}): EnvInherit;Env.inherit reads from the worker (.boboddy/.env, then process.env), not from the devcontainer’s containerEnv. A bare string and Env.value are stored in the definition in plaintext; inherit stores only the name (and default). See the forms table for what is stored and masked.
secret: true together with default is a type error. Optional entries without a default are typed string | undefined in the prompt. The key must match /^[A-Z_][A-Z0-9_]*$/; BOBODDY_*, HOME, XDG_CONFIG_HOME, XDG_DATA_HOME, OPENCODE_CONFIG_CONTENT, and npm_config_cache are reserved, and inherit cannot read BOBODDY_* worker variables. A step may declare at most 64 variables of at most 4096 characters each.
Defining a step throws on a static secret, and on a bare static string whose key matches /SECRET|TOKEN|PASSWORD|PASSWD|API_?KEY|PRIVATE/i. The wire shape is envJson, validated by envVarsSchema from @boboddy/sdk/env-vars.
See Environment variables for precedence, per-runtime behaviour, and caveats.
Repo builds the environment.repo value: what the worker does with the repository clone. It is exported from @boboddy/sdk. Omit repo for the runtime’s default.
import { defineStep, Repo } from "@boboddy/sdk";import { z } from "zod";
defineStep({ key: "triage", name: "Triage", environment: { repo: Repo.readOnly() }, agentPrompt: "Find the code behind this report.",});
defineStep({ key: "implement", name: "Implement", result: z.object({ summary: z.string() }), environment: { repo: ({ result }) => Repo.readWrite({ message: `fix: ${result.summary}`, onPushFailure: "warn", }), }, agentPrompt: "Fix the reported bug.",});type RepoSpec = NoneRepoSpec | ReadOnlyRepoSpec | ReadWriteRepoSpec;
Repo.readWrite(opts?: { message?: string; // {{input.…}} / {{result.…}} tokens; one line, at most 500 characters onPushFailure?: "fail" | "warn"; // defaults to "fail"}): ReadWriteRepoSpec;
Repo.readOnly(): ReadOnlyRepoSpec;
Repo.none(): NoneRepoSpec;| Constructor | Behaviour |
|---|---|
Repo.readWrite() |
Creates a work branch, then commits and pushes it when the step succeeds. The default for Runtime.devcontainer(). |
Repo.readOnly() |
Clones and checks out the base branch. No work branch, no commit, no push, and no workBranch reported. A policy, not a sandbox: edits stay in the workspace and are discarded. |
Repo.none() |
No repository: no clone, branch, commit or push. The default for Runtime.host(). |
Which repo each runtime accepts:
| Runtime | readWrite |
readOnly |
none |
Default |
|---|---|---|---|---|
Runtime.devcontainer() |
Yes | Yes | No | readWrite |
Runtime.managed.<id>() |
Yes | Yes | No | readWrite |
Runtime.host() |
No | No | Yes | none |
A pair the runtime rejects is a type error where TypeScript can express it, and otherwise a validation error at definition time, at push, and from the server (422). codeStep() accepts repo with the same rules.
repo can be a function of { input, result }, run once at definition time against the same proxies agentPrompt uses, so a template literal built from them becomes a {{result.…}} or {{input.…}} token that the worker renders. Start the string with a prefix (`fix: ${result.summary}`): a bare `${result.summary}` trips a lint rule. A static message string with the tokens written out works too.
message rules:
- Only
inputandresulttokens are allowed.{{env.…}}is rejected so environment values cannot reach git history. - The rendered subject has whitespace collapsed and control characters stripped, is truncated to 200 characters, and falls back to
boboddy: step <stepExecutionId>when empty.
onPushFailure defaults to "fail": a failed push of the work branch or of a submodule fails the step. "warn" logs the failure and lets the step finish, but the step reports no work branch, so later steps do not build on it. See Work branches for the behavior change and its consequences.
The wire form is a nested repo object, { mode: "none" | "readOnly" | "readWrite", message?, onPushFailure? }. The SDK emits it only when the author wrote a repo; the server resolves an omitted one to the runtime’s default and stores it fully resolved (message is null for the default). The schemas are repoConfigInputSchema and repoConfigSchema, with resolveRepoConfig, from @boboddy/sdk/repo-config.
See Repo access for the guide.
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.
healthChecks
Section titled “healthChecks”type HealthCheck = { tool: string; mcp?: string; // one of the step's declared mcpServers keys name?: string; // defaults to the resolved tool id args?: Record<string, unknown>; severity?: "required" | "warn"; // default: "required" timeoutMs?: number; // default: 15000 serialGroup?: string; // checks sharing a value run serially, in declaration order};Every check runs in parallel by default, each in its own lane. Give two or more checks the same serialGroup to force them to run one at a time, in declaration order, against each other — while still running concurrently with every other check/group. required checks all run before any warn check; the first required failure stops every check that hasn’t started yet, but one already in flight still finishes and reports its real outcome.
See Health checks for a full example and the no-secrets-in-args guidance.
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() |
A real specialization of notifications() (not an alias): narrows every emitted item to kind: "feedback_request", backed by the same $boboddy_notifications_v1 signal. |
Features is exclusively for features: [...] attachment — value construction (Notify) and signal reading (NotificationSignal) are separate exports, kept apart on purpose so Features. autocomplete only ever shows things you attach to a step.
Each notification item is:
// Inferred from the same zod schema `resultSchemaJson` is built from —// there is no independently hand-written type to drift out of sync.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? }};NotificationSignal
Section titled “NotificationSignal”NotificationSignal.key is the raw $boboddy_notifications_v1 signal key. NotificationSignal.find(signals) parses a step execution’s signals array back into NotificationItem[] | undefined.
Notify
Section titled “Notify”A flat namespace of builder functions for constructing a notification result value at runtime — the same shape as Rule/Computed in @boboddy/sdk/definitions/pipelines — without hand-writing the object shape. Most useful inside a codeStep()’s fn, where there’s no agent to follow the prompt instructions.
Notify.create(item: NotificationItem);Notify.inApp(title, body, priority, options?);Notify.feedbackRequest(question, category, urgency, suggestedKey?);Notify.merge(...fragments);createis the one generic constructor — field names matchNotificationItemexactly.inAppis the only channel with dedicated sugar, becausein_appis the only channel the platform actually delivers today;work_item_platform_comment,email, andslackare accepted by the schema as suggestions but currently have no delivery adapter. UseNotify.create({ ..., suggestedChannels: [...] })directly to suggest one of those.feedbackRequestis the value-builder counterpart toFeatures.feedbackRequests().- Each of
create/inApp/feedbackRequestreturns{ $boboddy_notifications_v1: [item] }— return it directly, or spread it into a larger result.inApp’soptionsis{ kind?, payload? };kinddefaults to"status_update". merge(...fragments)combines multiple builder results into one.
See Features for the guide.
codeStep(options)
Section titled “codeStep(options)”Defines a kind: "code" step: a plain function instead of an LLM prompt. Produces the same step shape defineStep() does, so it plugs into a states[key].step field unchanged.
import { codeStep } from "@boboddy/sdk/definitions/steps";import { z } from "zod";
export const sumScores = codeStep({ key: "sum-scores", name: "Sum Scores", inputSchema: z.object({ scores: z.array(z.number()) }), resultSchema: z.object({ total: z.number() }), fn: ({ scores }) => ({ total: scores.reduce((a, b) => a + b, 0) }), signals: [{ sourcePath: "total", key: "total", type: "number" }], status: "active",});DefineCodeStepInput options
Section titled “DefineCodeStepInput 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 |
fn |
(input) => result | Promise<result> |
Yes | The step’s implementation; may be inline. The worker imports the module and finds the step by key, so the step can be exported by name, embedded in a pipeline, or both. See Code steps |
inputSchema |
ZodType |
No | Zod schema for fn’s input |
resultSchema |
ZodType |
No | Zod schema for fn’s return value |
signals |
CodeStepSignalSpec[] |
No | Values to extract from the result; unlike defineStep, type is required (not inferred) |
features |
AnyStepFeature[] |
No | Feature plugins from Features; only each feature’s result extension and signals apply (no prompt on a code step) |
environment |
{ runtime?: DevcontainerRuntimeSpec | ManagedRuntimeSpec; vars?: ({ input }) => Record<string, string | EnvEntry>; repo?: DevcontainerRepoSpec | (({ input, result }) => DevcontainerRepoSpec) } |
No | runtime is Runtime.managed.<id>() or Runtime.devcontainer({ config? }); the default is Runtime.managed.bun1(). Runtime.host() is not supported. vars are environment variables for the runner process, read by fn through process.env. repo is Repo.readWrite() (default) or Repo.readOnly(). See Runtime, Env and Repo |
status |
"draft" | "active" |
No | Draft steps are not executed; defaults to "active" |
CodeStepSignalSpec is { sourcePath, key?, type: "string" | "number" | "boolean" | "object" | "array", required?, availableWhenResultStatusIn? }.
definePipeline(config)
Section titled “definePipeline(config)”Define a pipeline as a flat map of named states — see Building Pipelines for the full guide.
import { definePipeline } from "@boboddy/sdk/definitions/pipelines";import { z } from "zod";
const myPipeline = definePipeline({ key: "my-pipeline", name: "My Pipeline", status: "active", input: z.object({ text: z.string() }), startAt: "run", states: { run: { kind: "step", step: myStep, input: (ctx) => ({ text: ctx.pipelineInput("text") }), next: "done", }, done: { kind: "succeed" }, },});DefinePipelineInput options
Section titled “DefinePipelineInput options”| Field | Type | Required | Description |
|---|---|---|---|
key |
string |
Yes | Unique pipeline key |
name |
string |
No | Display name (default: key) |
version |
number |
No | Version (default: 1) |
description |
string |
No | Short description |
status |
"draft" | "active" |
No | Draft pipelines are not executed |
input |
ZodType |
No | The pipeline’s own input schema; drives ctx.pipelineInput(path)’s typing |
startAt |
string |
Yes | The entry state’s key. Must not name a choice/succeed/fail state |
states |
Record<string, PipelineState> |
Yes | Every state in the pipeline, keyed by its unique state key |
PipelineState
Section titled “PipelineState”A discriminated union on kind, keyed by each state’s own map key (there is no separate dependsOn — every incoming edge is derived from another state’s own forward pointer):
| Kind | Shape |
|---|---|
step |
{ kind: "step", step, input?, timeout?, blockWhen?, next } |
choice |
{ kind: "choice", choices?: [{ when, next }], default? } |
fanOut |
{ kind: "fanOut", step, over, maxConcurrency?, input?, timeout?, advanceEach, advanceAll, next } |
parallel |
{ kind: "parallel", branches: Record<string, { step, input?, timeout? }>, advanceAll?, next } |
loop |
{ kind: "loop", step, maxIterations, input?, timeout?, until, next, onExhausted } |
split |
{ kind: "split", branches: string[] } |
succeed |
{ kind: "succeed" } |
fail |
{ kind: "fail" } |
next on a step/fanOut/parallel/loop-exit accepts either a state key (string) or { routeToPipeline: string, input?: Record<string, unknown> | null } to hand execution off to a different pipeline. See States for a field-by-field breakdown of each kind, and Pipeline Advancement for blockWhen/choices[].when/until and advanceEach/advanceAll.
NodeInputCtx — the input mapper’s context
Section titled “NodeInputCtx — the input mapper’s context”Every step/fanOut/parallel-branch/loop’s input mapper receives:
| Method | Description |
|---|---|
ctx.pipelineInput(path) |
Reads a dot-path out of the pipeline’s own input schema |
ctx.workItem.<property> / .field(name) |
Reads a top-level work-item property (id, platform, title, url, etc.) or a fields-bag field directly |
ctx.signal(nodeKey, signalKey) |
An earlier state’s signal, addressed by that state’s own key |
ctx.output(nodeKey) |
An earlier state’s whole result output |
ctx.signalsList(nodeKey) |
A fan-out’s whole cohort (every terminal branch’s signals + output) |
ctx.literal(value) |
A fixed value that doesn’t come from anywhere at runtime |
ctx.item |
fanOut branches only — the current branch’s own item |
workItemTitle and workItemDescription are bound automatically on every state; there is no separate pipeline-level default-binding layer — every mapper must supply everything it needs itself.
Rule and Computed
Section titled “Rule and Computed”Exported from @boboddy/sdk/definitions/pipelines, used by blockWhen, a choice branch’s when, and a loop’s until:
Rule.when(signalKey, operator, value); // bare conditionRule.signal(signalKey, operator, value); // leaf, for nesting in all/anyRule.all([condition, condition]);Rule.any([condition, condition]);operator is one of "equal" | "notEqual" | "lessThan" | "lessThanInclusive" | "greaterThan" | "greaterThanInclusive" | "in" | "notIn" | "contains" | "doesNotContain".
Computed.average / .weightedAverage / .sum / .min / .max / .count / .booleanAny / .booleanAll each take an array of two or more signal keys and return a token that can be passed as Rule.when/Rule.signal’s signal argument in place of a bare key. See Pipeline Advancement for the full reference and examples.
Fan-out and parallel cohort advancement
Section titled “Fan-out and parallel cohort advancement”A fanOut state’s advanceEach/advanceAll and a parallel state’s advanceAll use a fluent, comparator-chain context distinct from the Rule DSL — see the Fan-out guide for the state shapes and a full example.
advanceEach context
Section titled “advanceEach context”Evaluated per-branch against that branch’s own signals:
signal(key)/stepSignals.<key>— the branch’s own signal, typed against the step’s declared signals. Chain a comparator (.eq,.gt,.gte,.lt,.lte,.ne,.in,.notIn,.contains,.doesNotContain) then.then(outcome).all(...refs)/any(...refs)— group refs; terminate with.then(outcome).outcomeis restricted to"continue" | "block"(no routing).- No computed-signal factories (
Computed.average, 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 a comparator ref.branchOutcomes.count(outcome)— a single outcome’s count across the cohort (fact${outcome}Count). Returns a comparator ref.branchOutcomes.every(outcome)/.some(outcome)—trueiff every/any branch resolved tooutcome. Already a rule leaf — 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 a comparator ref.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.title.contains("[urgent]").then(assign(bugTriage)), 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.<property> |
Comparator ref for a top-level work item property (id, platform, title, url, etc.) |
workItem.field(name) |
Returns a comparator ref for the named work item field (fields bag), typed via field<TName, TValue> |
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 |
workItem’s top-level properties are id, projectId, platform, platformId, platformKey, url, title, description, sourceCreatedAt, sourceUpdatedAt, createdByUserId, parentWorkItemId, createdAt, updatedAt. platform is typed to the real platform literal union; the date-like fields are ISO strings (or null), not Date objects. See the pipeline assignment guide for details.
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. Not every comparator applies to every field — .gt/.gte/.lt/.lte only exist on numeric fields, so e.g. workItem.title.gt(...) is a compile error.
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();