// Props
import { Kanban } from "smthrs";
type ColumnDef = {
name: string;
agent: AgentLike;
output: OutputTarget;
prompt?: (ctx: { item: unknown; column: string }) => string;
task?: Omit<Partial<TaskProps>, "agent" | "children" | "id" | "key" | "output" | "smithersContext">; // retries, timeoutMs, etc.
};
type KanbanProps = {
id?: string; // default: "kanban"
columns: ColumnDef[];
useTickets: () => Array<{ id: string; [key: string]: unknown }>;
agents?: Record<string, AgentLike>; // overrides column-level agents
maxConcurrency?: number; // default: unlimited (no cap), per column
onComplete?: OutputTarget;
until?: boolean; // default: false
maxIterations?: number; // default: 5
skipIf?: boolean;
children?: ReactNode | Record<string, unknown>; // content for onComplete task
};
bunx smthrs graph):
/** @jsxImportSource smthrs */
import { createSmithers, Kanban, ClaudeCodeAgent } from "smthrs";
import { z } from "zod";
const { Workflow, smithers, outputs } = createSmithers({
triage: z.object({ note: z.string() }),
build: z.object({ note: z.string(), done: z.boolean() }),
});
const agent = new ClaudeCodeAgent({ model: "claude-sonnet-5" });
export default smithers(() => (
<Workflow name="ticket-board">
<Kanban
columns={[
{ name: "triage", agent, output: outputs.triage },
{ name: "build", agent, output: outputs.build },
]}
useTickets={() => [{ id: "T-1" }, { id: "T-2" }]}
maxIterations={2}
/>
</Workflow>
));
Notes
- Item tasks default to
continueOnFail={true}; usecolumn.taskto add retries or override. useTicketsis called at render time; return different items per iteration for dynamic sources.- Use
untilwithctx.outputMaybe()to exit when all items reach the final column.
Source
The<Kanban> implementation and the files it imports, straight from the package source. This section is generated; edit the source, not this block.
// @smithers-type-exports-begin
/** @typedef {import("./ColumnDef.ts").ColumnDef} ColumnDef */
/** @typedef {import("./KanbanProps.ts").KanbanProps} KanbanProps */
// @smithers-type-exports-end
import React from "react";
import { Sequence } from "./Sequence.js";
import { Parallel } from "./Parallel.js";
import { Loop } from "./Ralph.js";
import { Task } from "./Task.js";
/**
* <Kanban> — Process items through columns with pluggable ticket source.
*
* Composes Loop, Sequence, Parallel, and Task to create a board where items
* flow through columns. Each column processes items via its assigned agent.
* Items in the same column can be processed in parallel.
* @param {KanbanProps} props
*/
export function Kanban(props) {
if (props.skipIf) return null;
const {
id,
columns,
useTickets,
agents,
maxConcurrency,
onComplete,
until = false,
maxIterations = 5,
children,
} = props;
const prefix = id ?? "kanban";
const tickets = useTickets();
// Build a Sequence of columns. Each column processes all tickets in Parallel.
const columnElements = columns.map((col, colIdx) => {
const agent = agents?.[col.name] ?? col.agent;
const taskElements = tickets.map((item) => {
const taskId = `${prefix}-${col.name}-${item.id}`;
const taskProps = col.task ?? {};
const prompt = col.prompt
? col.prompt({ item, column: col.name })
: `Process item ${item.id} in column "${col.name}".`;
return React.createElement(Task, {
...taskProps,
key: `${col.name}-${item.id}`,
id: taskId,
output: col.output,
agent,
continueOnFail: taskProps.continueOnFail ?? true,
label: taskProps.label ?? `${col.name}: ${item.id}`,
children: prompt,
});
});
return React.createElement(
Parallel,
{
key: `col-${colIdx}-${col.name}`,
id: `${prefix}-col-${col.name}`,
maxConcurrency,
},
...taskElements,
);
});
const sequence = React.createElement(Sequence, null, ...columnElements);
const loop = React.createElement(
Loop,
{
id: `${prefix}-loop`,
until,
maxIterations,
onMaxReached: "return-last",
},
sequence,
);
if (!onComplete) {
return loop;
}
return React.createElement(
Sequence,
null,
loop,
React.createElement(Task, {
key: `${prefix}-complete`,
id: `${prefix}-complete`,
output: onComplete,
label: "Board complete",
children: children ?? null,
}),
);
}
import React from "react";
/** @typedef {import("./SequenceProps.ts").SequenceProps} SequenceProps */
/**
* @param {SequenceProps} props
*/
export function Sequence(props) {
if (props.skipIf) return null;
// Sequence carries only a display label; pass a sanitized bag (align with
// the sanitizing structural components) so control props don't leak through.
// `label` names the phase group in run views (graph, the Claude /workflows
// mirror) and is preserved in the persisted frame XML.
const next = {
...(props.label === undefined ? {} : { label: props.label }),
...(props.failurePolicy === undefined ? {} : { failurePolicy: props.failurePolicy }),
};
return React.createElement("smithers:sequence", next, props.children);
}
import React from "react";
/** @typedef {import("./ParallelProps.ts").ParallelProps} ParallelProps */
/**
* @param {ParallelProps} props
*/
export function Parallel(props) {
if (props.skipIf) return null;
// Align prop sanitization with other structural components. `label` names
// the phase group in run views (graph, the Claude /workflows mirror).
const next = {
maxConcurrency: props.maxConcurrency,
subtreeConcurrency: props.subtreeConcurrency,
id: props.id,
...(props.label === undefined ? {} : { label: props.label }),
...(props.priority === undefined ? {} : { priority: props.priority }),
...(props.failurePolicy === undefined ? {} : { failurePolicy: props.failurePolicy }),
};
return React.createElement("smithers:parallel", next, props.children);
}
// @smithers-type-exports-begin
/** @typedef {import("./RalphProps.ts").RalphProps} RalphProps */
// @smithers-type-exports-end
import React from "react";
/** @typedef {import("./LoopProps.ts").LoopProps} LoopProps */
/**
* @param {LoopProps} props
*/
export function Loop(props) {
if (props.skipIf) return null;
// Sanitize to the loop's host props (align with other structural components);
// key/skipIf are React/control props and children are passed separately.
const next = {
id: props.id,
until: props.until,
maxIterations: props.maxIterations,
onMaxReached: props.onMaxReached,
continueAsNewEvery: props.continueAsNewEvery,
};
return React.createElement("smithers:ralph", next, props.children);
}
/** @deprecated Use `Loop` instead. */
export const Ralph = Loop;
// @smithers-type-exports-begin
/**
* @template D
* @typedef {import("./InferDeps.ts").InferDeps<D>} InferDeps
*/
/** @typedef {import("./OutputTarget.ts").OutputTarget} OutputTarget */
// @smithers-type-exports-end
import { applyCliToolAllowlist } from "./cliToolAllowlist.js";
import { createTaskComponent } from "./taskCore.js";
export { renderPromptToText } from "./taskCore.js";
/**
* The Node/CLI-agent-aware `Task`. Every render-path behavior (deps
* resolution, agent-chain assembly, MDX prompt rendering, static/compute/agent
* branching) lives in `taskCore.js`; this file only supplies the CLI-agent
* tool-allowlist enforcement step (`applyCliToolAllowlist`, which statically
* imports `ClaudeCodeAgent`/`PiAgent`/`GeminiAgent`/`AntigravityAgent` — see
* `cliToolAllowlist.js`). `Task.browser.js` builds the same component with a
* no-op allowlist step instead, so it never pulls those Node-only
* (`node:child_process`-backed) classes into a browser bundle.
*/
export const Task = createTaskComponent({ applyCliToolAllowlist });
import type { AgentLike } from "@smthrs/agents/AgentLike";
import type { TaskProps } from "./TaskProps.ts";
import type { OutputTarget } from "./OutputTarget.ts";
type ColumnTaskProps = Omit<
Partial<TaskProps<unknown>>,
"agent" | "children" | "id" | "key" | "output" | "smithersContext"
>;
export type ColumnDef = {
name: string;
agent: AgentLike;
/** Output schema for tasks in this column. */
output: OutputTarget;
/** Prompt template. Receives `{ item, column }` and returns a string. */
prompt?: (ctx: { item: unknown; column: string }) => string;
/** Optional Task props applied to each generated item task in this column. */
task?: ColumnTaskProps;
};
import type React from "react";
import type { AgentLike } from "@smthrs/agents/AgentLike";
import type { ColumnDef } from "./ColumnDef.ts";
import type { OutputTarget } from "./OutputTarget.ts";
export type KanbanProps = {
id?: string;
/** Column definitions in order. Items flow left to right. */
columns: ColumnDef[];
/** Function that returns ticket items to process. Each item must have an `id` field. */
useTickets: () => Array<{
id: string;
[key: string]: unknown;
}>;
/** Record mapping column names to agents. Overrides column-level agents. */
agents?: Record<string, AgentLike>;
/** Max items processed in parallel per column. */
maxConcurrency?: number;
/** Callback output schema when an item reaches the final column. */
onComplete?: OutputTarget;
/** Whether the board loop is done. When true, the loop exits. */
until?: boolean;
/** Max iterations through the column pipeline. */
maxIterations?: number;
skipIf?: boolean;
children?: React.ReactNode | Record<string, unknown>;
};