Workflow Model
workflow-code supports three project types: workflow, conversation, and kanban. Workflow and Conversation projects are self-contained TypeScript workflows. Their hosts (the platform, CLI, Server, and Desktop) provide the global workflow namespace, so project code must not import runtime APIs from "workflow-code". 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. workflowCode.projectInfo also stores localized names, manual display declarations, display platforms, data location, and related projects:
{
"name": "Default Name",
"workflowCode": {
"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. A Web display declaration does not enable the Server executor. 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 workflowCode.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:
workflow.defineWorkflow
workflow.defineEntrypoint
workflow.defineExecutor
workflow.parseConversationDefaultInputArgs
workflow.createInputNode
workflow.createFileInputNode
workflow.createUserInputNode
workflow.createToolApprovalGate
workflow.createLLMNode
workflow.createLLMStreamNode
workflow.createLLMProviderFromEnv
workflow.createLLMProviderRef
workflow.createOutputNode
workflow.createDetailsOutputNode
workflow.createOutputItem
workflow.createOutputPayload
workflow.addOutputItem
workflow.createOutputItemChunk
workflow.createTimerNode
workflow.endTimer
workflow.createQuestionClassifierNode
workflow.runNode
workflow.runStreamNode
workflow.runIf
workflow.runFor
workflow.runWhile
workflow.runWorkflow
workflow.listKnowledgeDocuments
workflow.getKnowledgeDocument
workflow.createKnowledgeDocument
workflow.editKnowledgeDocument
workflow.deleteKnowledgeDocument
workflow.searchKnowledgeDocuments
workflow.readKnowledgeDocumentLines
workflow.getEnv
workflow.getRuntimePlatform
workflow.getRuntimeLocale
workflow.isCli
workflow.isWeb
workflow.isDesktop
workflow.getLLMProvider
workflow.WorkflowError
workflow.getRuntimeLocale() returns "zh-CN" or "en" selected by the current CLI, Desktop, or Server 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 workflow.defineWorkflow<Input, Output>():
export const exampleWorkflow = workflow.defineWorkflow<Input, OutputPayload>({
name: "example",
async run(input, context) {
const parsed = await workflow.runNode(inputNode, input, context);
const result = await workflow.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 = workflow.createInputNode<Input, Parsed>({
name: "input",
parse(input) {
return { message: input.message.trim() };
},
});
const outputNode = workflow.createOutputNode<Parsed, OutputPayload>({
name: "output",
async *stream(input) {
yield input.message;
},
format(stream) {
return workflow.createOutputPayload({
items: stream.items,
});
},
});
The current protocol of the output payload is WorkflowOutputPayload.items: WorkflowOutputItem[]. 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, or audio; objects and arrays should be placed directly into content, not stringify as Markdown code blocks in advance. audio content use { src, mimeType, name, size, durationMs?, format? }, and src can be a secure data:audio/...;base64,... or http(s) URL.
workflow.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 CLI, Desktop and server according to output standard node. workflow.createTimerNode will create a standard common node named timer to start business timing. workflow.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. workflow.runNode returns the output of the node;workflow.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 workflow.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. workflow.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. workflow.runFor and workflow.runWhile record the loop itself as a traceable node, keeping the loop body visible in Desktop Diagram.
workflow.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). Desktop, server embed and server API will be displayed in the same set of Run workspace forms. 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 workflow-code json --run-id <id> --resolved-user-inputs-json '<json>' injecting the answer.
workflow.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
workflow.runIf is suitable for writing business conditions as traceable workflow branches, rather than just plain TypeScript if:
const plan = await workflow.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 workflow.runNode, workflow.runStreamNode, or it can nest another workflow.runIf to form a multi-level branch.
Loop
workflow.runFor and workflow.runWhile are suitable for writing business loops as traceable workflow loops instead of just plain TypeScript for/while:
const rounded = await workflow.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 workflow.runNode(addRoundNode, { ...state, value, index }, loopContext);
},
});
const boosted = await workflow.runWhile(rounded, context, {
name: "Boost until target",
maxIterations: 8,
condition: (state) => state.total < state.target,
async run(state, loopContext, iteration) {
return workflow.runIf(state, loopContext, {
name: "Choose boost strategy",
branches: [
{
label: "burst",
condition: (current) => current.target - current.total >= 7,
async run(current, branchContext) {
return workflow.runFor(current, branchContext, {
name: "Burst passes",
maxIterations: 3,
items: () => [4, 3],
async run(passState, value, index, passContext) {
return workflow.runNode(boostNode, { ...passState, value, index, iteration }, passContext);
},
});
},
},
],
else: {
label: "finish",
async run(current, branchContext) {
return workflow.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
Desktop 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 asworkflow.createInputNode,workflow.defineWorkflow,workflow.getEnv. 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 ofworkflow.runNode/workflow.runStreamNode/workflow.runIf/workflow.runFor/workflow.runWhile/workflow.runWorkflow. Only nodes that are actually executed by these API s will show running, success, failure, elapsed time, and errors.
workflow.runIf will be displayed as a run-if branch node, and Desktop 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.
workflow.runFor and workflow.runWhile are exposed as run-for / run-while loop nodes. Desktop 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 workflow.runIf, workflow.runFor, or workflow.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.
workflow.runWorkflow 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 workflow.runWorkflow() 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 = workflow.createLLMProviderRef("default", () => {
return workflow.createLLMProviderFromEnv();
});
Business nodes rely on the unified provider interface and are not directly bound to specific manufacturers. createLLMProviderFromEnv reads LLM_TYPE, LLM_API_KEY, LLM_BASE_URL, and LLM_MODEL from the workflow runtime environment.
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 = workflow.defineExecutor<Input, OutputPayload>({
projectType: "workflow",
defaultEntrypoint: "main",
entrypoints: [
workflow.defineEntrypoint<Input, OutputPayload>({
id: "main",
title: "Complete workflow",
workflow: exampleWorkflow,
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. Legacy top-level Workflow execution fields are no longer accepted.
Projects can use package.json.workflowCode.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 Desktop and Server Web. 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.workflowCode.projectType and cannot forge executor.
createInput receives workflowName, workflowDir, args, paramValues, env, abortSignal, and optional conversationId. Workflow also receives the selected entrypointId, entrypointTitle; the same fields are written to WorkflowContext.metadata, report, and the run record. Desktop, CLI, server and external API all finally convert the parameters to workflow input through the selected execution configuration; When the local or server runtime is canceled,abortSignal will be propagated to the workflow context, which is convenient for long-time provider/SDK calls to stop in time.
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 = workflow.defineExecutor<Input, OutputPayload>({
projectType: "workflow",
defaultEntrypoint: "main",
entrypoints: [
workflow.defineEntrypoint({
id: "main",
title: "Complete workflow",
workflow: exampleWorkflow,
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);
},
}),
],
});
The /api/workflows/{name}/debug/nodes/{nodeName} of the server will run single-node debugging using the node configuration in the selected entrypoint registry. When the entry is omitted, the default entry is used and the node with the same name is not searched across the entry. Conversation Continue to use the executor top-level registry.
Package structure
The workflow package declares the dev dependency of Core and CLI precisely, while declaring real third-party dependencies on demand. CLI, Desktop, and server provide the outer runtime:
{
"id": "1f221460-4fc7-48e2-bd23-651dee826692",
"name": "@workflow-code/hello-workflow",
"version": "0.1.0",
"private": true,
"type": "module",
"devDependencies": {
"@workflow-code/cli": "0.2.0",
"workflow-code": "0.2.0"
},
"workflowCode": {
"projectInfo": {
"dataStorage": {
"mode": "both"
},
"relatedProjects": []
}
}
}
workflow-code must be an exact SemVer; ^, 0.2.x, workspace:*, and other ranges are rejected. Server, Desktop, 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. @workflow-code/cli is also pinned exactly but versioned independently and does not replace Core runtime compatibility. Core 0.2 projects must also declare workflowCode.projectInfo.dataStorage.mode explicitly.
The minimum package of Kanban uses workflowCode.projectType: "kanban", and the directory only requires package.json + index.html; parameterized items are added with optional kanban.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.