Skip to content
boboddy.dev

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.


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",
});
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 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.
`,
});
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.artifactsDir for 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 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.json or .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.

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 input and result tokens 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.

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 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.

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 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 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.key is the raw $boboddy_notifications_v1 signal key. NotificationSignal.find(signals) parses a step execution’s signals array back into NotificationItem[] | undefined.

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);
  • create is the one generic constructor — field names match NotificationItem exactly.
  • inApp is the only channel with dedicated sugar, because in_app is the only channel the platform actually delivers today; work_item_platform_comment, email, and slack are accepted by the schema as suggestions but currently have no delivery adapter. Use Notify.create({ ..., suggestedChannels: [...] }) directly to suggest one of those.
  • feedbackRequest is the value-builder counterpart to Features.feedbackRequests().
  • Each of create/inApp/feedbackRequest returns { $boboddy_notifications_v1: [item] } — return it directly, or spread it into a larger result. inApp’s options is { kind?, payload? }; kind defaults to "status_update".
  • merge(...fragments) combines multiple builder results into one.

See Features for the guide.


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",
});
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? }.


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" },
},
});
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

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.

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 condition
Rule.signal(signalKey, operator, value); // leaf, for nesting in all/any
Rule.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.


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.

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).
  • outcome is 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.

Evaluated once per cohort, after every branch has settled:

  • branchOutcomes.total() — the cohort’s total branch count (fact branchCount). 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) — true iff every/any branch resolved to outcome. Already a rule leaf — go straight to .then(outcome).
  • stepSignalsList.pluck(signalKey) — seeds an aggregation across every branch’s signalKey value. 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).
  • outcome in .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.


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.

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.

Field Type Description
default AssignOutcome | SkipOutcome Outcome when no rule matches; assign(pipeline) or skip()
rules AssignmentRule[] Ordered rules; first match wins

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.


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");

Parse .boboddy/boboddy.jsonc files (JSON with comments):

import { parseJsonc } from "@boboddy/sdk";
const config = parseJsonc(rawString);

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();