Workflow Model
steamharness supports three project types: workflow, conversation, and kanban. Workflow and Conversation projects are self-contained TypeScript workflows. Their hosts (CLI and local hosts) provide the global workflow namespace, so project code must not import runtime APIs from "steamharness". Each project records exact Core and CLI versions in devDependencies: the Core version determines host compatibility, while the CLI is released independently. Kanban is a static project with index.html as its entry file and does not use a runtime API or executor. See Kanban HTML Project.
Project Information and Display Declarations
The root package.json is the sole source of truth for project information. Its required name is the default name and legacy-client fallback. steamHarness.projectInfo also stores localized names, manual display declarations, display platforms, data location, and related projects:
{
"name": "Default Name",
"steamHarness": {
"projectInfo": {
"localizedNames": {
"zh-CN": "中文名称",
"en": "English Name"
},
"declarations": ["multilingual", "multiplatform"],
"platformSupport": {
"mode": "specific",
"platforms": ["macos", "windows", "web"]
},
"dataStorage": { "mode": "both" },
"relatedProjects": []
}
}
}
localizedNames currently manages Simplified Chinese and English. Interfaces prefer the current-locale name, fall back to the root name, and search the default, Chinese, and English names together. declarations contains author-selected display labels only: hosts neither infer them from names or platforms nor verify that the project implements the declared capability. The current built-in declarations are multilingual and multiplatform.
platformSupport also describes display compatibility only; it does not decide where a version can run. Use { "mode": "agnostic" } for a platform-agnostic project, or specific with at least one of macos, windows, or web. Server stores source and data independently of display platforms. Readers and writers preserve unknown future locale, declaration, and platform IDs. A legacy project without projectInfo has no declarations, is platform agnostic, and uses only the root name.
relatedProjects is shared by Workflow, Conversation, and Kanban. It is not private to the assistant or Kanban. Each item contains an alias, target-project UUID, and grantToRelatedProject read/write rules; the grant means "allow the target project to access this project." A relationship is confirmed only after both packages point to each other. A one-sided declaration is pending and grants no KV access. See KV and session for the full format, validation limits, and access semantics. Removed steamHarness.kanban.dataSources is not converted and fails validation directly.
Runtime API
The current workflow global object comes from src/runtime-api.ts in the Core repository and contains:
steamharness.defineProject
steamharness.defineEntrypoint
steamharness.defineExecutor
steamharness.parseConversationDefaultInputArgs
steamharness.createInputNode
steamharness.createFileInputNode
steamharness.createUserInputNode
steamharness.createToolApprovalGate
steamharness.createLLMNode
steamharness.createLLMStreamNode
steamharness.createLLMProviderFromConfig
steamharness.createLLMProviderRef
steamharness.createOutputNode
steamharness.createDetailsOutputNode
steamharness.createOutputItem
steamharness.createOutputPayload
steamharness.addOutputItem
steamharness.createOutputItemChunk
steamharness.createTimerNode
steamharness.endTimer
steamharness.createQuestionClassifierNode
steamharness.runNode
steamharness.runStreamNode
steamharness.runIf
steamharness.runFor
steamharness.runWhile
steamharness.runProject
workflow.listKnowledgeDocuments
steamharness.getKnowledgeDocument
steamharness.createKnowledgeDocument
workflow.editKnowledgeDocument
workflow.deleteKnowledgeDocument
workflow.searchKnowledgeDocuments
steamharness.readKnowledgeDocumentLines
steamharness.getConfig
steamharness.getRuntimePlatform
steamharness.getRuntimeLocale
steamharness.isCli
steamharness.isDesktop
steamharness.getLLMProvider
steamharness.HarnessError
steamharness.getRuntimeLocale() returns "zh-CN" or "en" selected by the local host. It defaults to "zh-CN" when a host does not inject a language. A project that explicitly supports localization should apply this value consistently to visible output, errors, date formatting, and number formatting rather than inferring a browser or machine language. Static package, entrypoint, and parameter manifests retain the project's default language.
See Core API for complete methods, parameters, and runtime objects.
Workflow
A Workflow defines the business process through steamharness.defineProject<Input, Output>():
export const exampleProject = steamharness.defineProject<Input, OutputPayload>({
name: "example",
async run(input, context) {
const parsed = await steamharness.runNode(inputNode, input, context);
const result = await steamharness.runStreamNode(outputNode, parsed, context);
return result.output;
},
});
run receives input and context and returns the final payload. If it throws, the executor serializes the error into the execution report.
Node
Node is the smallest executable unit, which is divided into normal nodes, streaming nodes, branch nodes, and loop nodes:
const inputNode = steamharness.createInputNode<Input, Parsed>({
name: "input",
parse(input) {
return { message: input.message.trim() };
},
});
const outputNode = steamharness.createOutputNode<Parsed, OutputPayload>({
name: "output",
async *stream(input) {
yield input.message;
},
format(stream) {
return steamharness.createOutputPayload({
items: stream.items,
});
},
});
The current protocol of the output payload is RunOutputPayload.items: OutputItem[]. Each item is an independently presentable and collapsible message block containing id, title, content, optional icon/iconPreset, contentType, collapsed, and metadata. contentType only determines how to render: markdown, text, json, audio, or video; objects and arrays should be placed directly into content, not stringified as Markdown code blocks in advance. audio content uses { src, mimeType, name, size, durationMs?, format? }, and src can be a safe data:audio/...;base64,... or http(s) URL. video content uses { src, mimeType: "video/mp4", name, durationMs?, format?: "mp4" }, and src only accepts a data:video/mp4;base64,... value or a publicly reachable http(s) URL; the UI plays it through the native <video controls preload="metadata" playsInline> element. Signed OSS URLs fall under http(s) but are not cached, so do not publish generated video assets only as signed OSS URLs without a stable fallback. Missing required fields or an unsafe src cause a text-preview fallback that surfaces a localized message such as videoPlaybackUnsupported; never leave src empty and never combine non-http(s)/data schemes such as file:// or blob:.
steamharness.createDetailsOutputNode is a convenient node to create independent collapsible output item. It will convert { title, content, icon } to items: [{ title, content, icon, contentType, collapsed: true }] and still be collected by local CLI according to output standard node. steamharness.createTimerNode will create a standard common node named timer to start business timing. steamharness.endTimer(timer, context, { status, message }) explicitly ends timing and writes to report.timers. unfinished running timer will be automatically marked as cleared before workflow ends to avoid UI remaining running state. steamharness.runNode returns the output of the node;steamharness.runStreamNode returns { chunks, output }, where chunks is the raw streaming chunk array and output is the final payload generated by finalize or format. The format of the streaming createOutputNode({ stream, format }) receives the accumulated { text, items, chunks }; string chunk is automatically written to a default Markdown item, and the structured output can be sent using steamharness.createOutputItemChunk(...) output_item_start, output_item_delta, output_item_end. In addition to merging content, output_item_delta can also carry item patch updates title, icon, collapsed, contentType, or metadata.status to show the state change of the tool call from running to success/failed. steamharness.runIf checks the if / else-if / else branch in order, executes the corresponding run when hit, and logs the selected branch with the branch-node run event. steamharness.runFor and steamharness.runWhile record the loop itself as a traceable node, keeping the loop body visible in local host Diagram.
steamharness.createUserInputNode inserts a manually entered point in workflow that can be permanently restored. The params of the node uses the same field definition as the current Workflow entrypoint(Conversation is executor). CLI Web renders these fields in its local run form. When the external agent dynamically generates problems, the title, description, fields and default values can be generated for this request through resolveRequest(input, context) without side effects. If the value is not committed during the first execution,runtime issues a user_input_requested event,report writes pendingUserInput, and the run status becomes waiting_for_input; after submitting the answer, the same runId will resume execution with resolvedUserInputs and issue user_input_resolved. Workflow The exact target and the original entry in the run snapshot are used for restoration. The entry cannot be switched. CLI does not read stdin, and will output a waiting report when encountering this node. You need to continue by steamharness json --run-id <id> --resolved-user-inputs-json '<json>' injecting the answer.
steamharness.createToolApprovalGate uses the same set of wait/resume agreement approval tool calls. Workflow first declares the tool in tools of the current entrypoint,Conversation uses executor tools, and then calls gate in the business code; If the tool is not registered, it will return input_validation, and if it is disabled, it will return tool_permission_denied. when manual confirmation is required, a tool-approval request will be generated and run will be suspended to waiting_for_input.
Branch
steamharness.runIf is suitable for writing business conditions as traceable workflow branches, rather than just plain TypeScript if:
const plan = await steamharness.runIf(seed, context, {
name: "Weapon branch",
branches: [
{
label: "high intensity assault",
condition: (mission) => mission.intensity >= 0.66,
run: (mission) => ({ ...mission, weapon: "ak47" }),
},
{
label: "stealth close quarter",
condition: (mission) => mission.stealth >= 0.58,
run: (mission) => ({ ...mission, weapon: "mp7" }),
},
],
else: {
label: "balanced rifle",
run: (mission) => ({ ...mission, weapon: "m4a1" }),
},
});
The first element in the branches array appears as if in Diagram, subsequent elements appear as else-if, and else appears as a fallback branch. The run of the branch can continue to call steamharness.runNode, steamharness.runStreamNode, or it can nest another steamharness.runIf to form a multi-level branch.
Loop
steamharness.runFor and steamharness.runWhile are suitable for writing business loops as traceable workflow loops instead of just plain TypeScript for/while:
const rounded = await steamharness.runFor(seed, context, {
name: "Round loop",
maxIterations: 6,
items: (state) => Array.from({ length: state.rounds }, (_, index) => index + 1),
async run(state, value, index, loopContext) {
return steamharness.runNode(addRoundNode, { ...state, value, index }, loopContext);
},
});
const boosted = await steamharness.runWhile(rounded, context, {
name: "Boost until target",
maxIterations: 8,
condition: (state) => state.total < state.target,
async run(state, loopContext, iteration) {
return steamharness.runIf(state, loopContext, {
name: "Choose boost strategy",
branches: [
{
label: "burst",
condition: (current) => current.target - current.total >= 7,
async run(current, branchContext) {
return steamharness.runFor(current, branchContext, {
name: "Burst passes",
maxIterations: 3,
items: () => [4, 3],
async run(passState, value, index, passContext) {
return steamharness.runNode(boostNode, { ...passState, value, index, iteration }, passContext);
},
});
},
},
],
else: {
label: "finish",
async run(current, branchContext) {
return steamharness.runNode(boostNode, { ...current, iteration }, branchContext);
},
},
});
},
});
runFor will be executed in the order of iterable generated by items, up to 1000 times by default;runWhile will check condition before each round starts, up to 100 times by default. Both run should return to the next round status. executionInfo of the completion event logs loopType, iterations, iterationCount, maxIterations, and completed.
Diagram and node protocol
local host diagram use two types of information to present workflow:
- Static structure from TypeScript source code analysis. All known
workflow.*runtime API calls are written todefinitions.apiCallsand exposed as static API nodes, such assteamharness.createInputNode,steamharness.defineProject,steamharness.getConfig. These nodes help understand which platform capabilities are used by the source code, but are not executed individually or strung into theInput -> Resultexecution path. - The running status is from the
nodeHooksreport ofsteamharness.runNode/steamharness.runStreamNode/steamharness.runIf/steamharness.runFor/steamharness.runWhile/steamharness.runProject. Only nodes that are actually executed by these API s will show running, success, failure, elapsed time, and errors.
steamharness.runIf will be displayed as a run-if branch node, and local host will use the branch icon and semantic color to distinguish from LLM, output, provider, question-classifier. executionInfo of the completion event logs selectedBranch, selectedBranchIndex, and description for Trace to see which branch was actually taken. Static structural analysis prioritizes branches / else in the form of object literals; dynamic assembly branches can still run, but Diagram cannot fully expand all static paths.
steamharness.runFor and steamharness.runWhile are exposed as run-for / run-while loop nodes. local host Diagram will wrap the loop body children with a rectangular background container with semantic color. The loop head node can still be clicked to view items, condition, maxIterations and runtime iteration information. Loop bodies can continue to nest steamharness.runIf, steamharness.runFor, or steamharness.runWhile to express internal while branches and multi-level loops. Static structural analysis prioritizes the identification of loop options in the form of object literals; dynamic assembly options can still run, but Diagram cannot fully expand the loop body.
steamharness.runProject is shown as a run-workflow invocation node. The child workflow does not create an independent run report, and the child node events will enter the parent trace; the top-level executor entry will not insert this package node. Default conversationMode: "shared", kvMode: "shared", outputVisibility: "visible", passing only name does not change session or output semantics; when the parent workflow passes in conversationMode: "shared-readonly", the child workflow can read the parent session snapshot, but does not append transcript or modify the title. When the child workflow will write the exclusive KV state, it can pass in the kvMode: "isolated" at the same time.
Entrypoint is not a nested node in Diagram and does not express a parent-child relationship. The project exposes only one layer of independent entry. When multi-level business orchestration is required, the steamharness.runProject() is continuously nested. The default maximum depth is 16.
The normal nodes returned by the built-in node factory inherit BaseNode and the streaming nodes inherit BaseStreamNode; together they provide common information such as name, standardName, metadata, and getExecutionInfo(). Diagram does not bind the BaseNode class directly, but reads the NodeDefinition / StreamNodeDefinition execution protocol and runs the report, so future custom nodes can be executed and recorded as long as they conform to the protocol.
Provider
The only built-in provider name for the current executor is "llm". LLM provider through createLLMProviderRef delay parsing:
const llmProvider = steamharness.createLLMProviderRef("default", () => {
return steamharness.createLLMProviderFromConfig();
});
Project business values come from Project configurations, without project env loading or process-variable fallback. Host deployment and transport settings remain separate.
Executor
An executor is the project's static execution configuration. Hosts use it to discover the project and run it. A Workflow project declares one default entrypoint and any number of independently runnable entrypoints:
export const executor = steamharness.defineExecutor<Input, OutputPayload>({
projectType: "workflow",
defaultEntrypoint: "main",
entrypoints: [
steamharness.defineEntrypoint<Input, OutputPayload>({
id: "main",
title: "Complete workflow",
group: { id: "operations", title: "Operations", order: 10 },
workflow: exampleProject,
params: [
{
name: "message",
flag: "--message",
type: "string",
required: true,
description: "Input text.",
},
],
createInput({ args }) {
return parseArgs(args);
},
}),
],
});
Selecting or requesting an entrypoint creates a run only for that entrypoint; it does not run main first. Entry IDs are unique within the project and must match /^[a-z][a-z0-9_-]{0,63}$/. entrypoints must be a non-empty static array, and defaultEntrypoint must name one of its entries. Each entrypoint independently declares its workflow, params, resolveParams, createInput, provider, tools, token usage, registry, and context factory. An entrypoint may also declare static group: { id, title, order? } metadata: id and title must be non-empty strings and order must be a non-negative integer. All entries sharing a group ID must use exactly the same title and order, including whether order is omitted. Legacy top-level Workflow execution fields are no longer accepted.
When any explicit group exists, the local Run workspace renders entrypoints in collapsible groups. Explicit orders sort ascending with first-seen order as the tie breaker; groups without an order follow in first-seen order, and entries without a group use a localized Ungrouped group. The default group is open on first entry, and starting a run or receiving a new active/result entry reopens its group. Only group expansion is persisted by stable projectId + resolved target. Projects without groups retain the existing flat layout. CLI and local schedules continue to use entrypoint IDs.
Projects can use package.json.steamHarness.schedulePresets to propose schedules for an entrypoint. Structure validation checks the preset ID, entrypoint, time zone, trigger rule, and JSON parameters, then exposes the presets to the local host. Presets are not enabled automatically. See Workflow scheduled runs.
Conversation executor keeps a single entry, places fields such as workflow, params, and createInput directly on executor, and does not declare or accept entrypoint. projectType of Executor only allows "workflow" or "conversation". Kanban declares "kanban" in package.json.steamHarness.projectType and cannot forge executor.
createInput receives projectName, projectDir, args, paramValues, config, abortSignal, and optional conversationId. Workflow also receives the selected entrypointId and entrypointTitle; the same fields are written to RunContext.metadata, reports, and local run records. The local host converts parameters to workflow input through the selected execution configuration. Cancellation propagates abortSignal to the workflow context so long-running provider and SDK calls can stop promptly.
resolveParams can return value, visible, disabled, and a single option status patch as the current parameter, or replace the current full list of options for the parameter with an resolvedOptions array, or restore the static options with null. Initialization and changes to the Form parameters of main, Advanced, and Quick will trigger evaluation. visible: false does not implicitly empty old values; mutex parameters should also return value: null to prevent hidden values from continuing into args and paramValues. Dynamic lists are suitable for reading provider, model, or variant directories provided by the server; reads must be cancelable, time-limited, and run data cannot be written during parameter evaluation.
Registry
Registry is used for single-node debugging and structure demonstration. The registry of Workflow belongs to the concrete entrypoint and must be a static array literal:
export const executor = steamharness.defineExecutor<Input, OutputPayload>({
projectType: "workflow",
defaultEntrypoint: "main",
entrypoints: [
steamharness.defineEntrypoint({
id: "main",
title: "Complete workflow",
workflow: exampleProject,
registry: [
{
name: "output",
node: outputNode,
stream: true,
metadata: { type: "output", title: "Output", version: "1.0.0" },
createInput({ args }) {
return parseArgs(args);
},
},
],
createInput({ args }) {
return parseArgs(args);
},
}),
],
});
Local CLI debug-node uses the selected entrypoint registry to debug one node. Omitting the entry selects the default and never searches other entrypoints. Conversation uses its executor-level registry.
Package structure
The workflow package declares exact Core and CLI dev dependencies, plus any required third-party dependencies. CLI and local hosts provide the outer runtime:
{
"id": "1f221460-4fc7-48e2-bd23-651dee826692",
"name": "@steamharness/hello-workflow",
"version": "0.1.0",
"private": true,
"type": "module",
"devDependencies": {
"@steamharness/cli": "0.2.0",
"steamharness": "0.2.0"
},
"steamHarness": {
"projectType": "workflow",
"projectInfo": {
"dataStorage": {
"mode": "both"
},
"relatedProjects": []
}
}
}
steamharness must be an exact SemVer; ^, 0.2.x, workspace:*, and other ranges are rejected. Local hosts and CLI evaluate the project's required Core before execution but compare only major.minor: any patch or exact prerelease on that line runs directly, while a major or minor change requires a host upgrade. @steamharness/cli is also pinned exactly but versioned independently and does not replace Core runtime compatibility. Core 0.2 projects must also declare steamHarness.projectInfo.dataStorage.mode explicitly.
The minimum package of Kanban uses steamHarness.projectType: "kanban", and the directory only requires package.json + index.html; parameterized items are added with optional configuration.json. The Kanban package records the Core and CLI versions equally precisely, so that project management tools maintain a uniform dependency model;Kanban itself does not implement Core runtime.