JSON Workflow Runtime
Generate and execute agent workflows as portable JSON definitions.
Quick Example
Define a workflow as a plain JSON document and execute it with parseAndRunWorkflow:
import { parseAndRunWorkflow, AgentHarness, type Tool } from '@noetic-tools/core';
declare const webSearchTool: Tool;
const workflow = {
version: 1,
root: {
kind: 'sequence',
id: 'research-pipeline',
steps: [
{
kind: 'llm',
id: 'gather',
model: 'openai/gpt-4o',
instructions: 'Search for recent AI news',
tools: ['webSearch'],
},
{
kind: 'llm',
id: 'summarize',
instructions: 'Summarize the findings above',
},
],
},
};
const harness = new AgentHarness({ name: 'runner', params: {} });
const ctx = harness.createContext();
const result = await parseAndRunWorkflow({
json: workflow,
harness,
ctx,
tools: [webSearchTool],
});The JSON Workflow Runtime lets you express agent workflows as portable JSON documents that are validated, hydrated into live Steps, and executed by the AgentHarness. Because the format is pure JSON, workflows can be generated by LLMs at runtime, stored in databases, transferred over the wire, or version-controlled alongside configuration.
Workflow Document Format
Every workflow document is an envelope with a version field and a root node:
{
"version": 1,
"root": { "...": "WorkflowNode" }
}| Field | Type | Description |
|---|---|---|
version | 1 | Schema version. Currently only 1 is supported. |
root | WorkflowNode | The top-level node of the workflow tree. |
The root node and all descendant nodes are WorkflowNode objects, each identified by a kind discriminant and a unique id string. The document is validated at runtime by WorkflowDocumentSchema (a Zod schema exported from @noetic-tools/core).
Node Kinds
Nine node kinds are available. Leaf nodes (llm, tool) perform work; structural nodes (sequence, fork, branch, loop, spawn, provide, every) compose them into trees.
llm
Call an LLM with instructions and optional tools.
{
"kind": "llm",
"id": "summarize",
"model": "openai/gpt-4o",
"instructions": "Summarize the input text in three bullet points",
"tools": ["webSearch"],
"params": { "temperature": 0.3, "maxTokens": 1024 }
}| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique step identifier. |
model | string | 'openai/gpt-4o' | Model identifier (OpenRouter format). |
instructions | string | required | System/user prompt for the LLM. |
tools | string[] | undefined | Tool names resolved from the tool registry. |
params | object | undefined | Model parameters: temperature, topP, maxTokens, stopSequences. |
tool
Invoke a registered tool directly.
{
"kind": "tool",
"id": "calc-step",
"toolName": "calculator",
"args": { "expression": "2 + 2" }
}| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique step identifier. |
toolName | string | required | Name of a tool in the tool registry. |
args | Record<string, unknown> | {} | Arguments passed to the tool's execute function. |
sequence
Run steps sequentially, threading each output as the next step's input.
{
"kind": "sequence",
"id": "pipeline",
"steps": [
{ "kind": "llm", "id": "step-1", "instructions": "Extract key facts" },
{ "kind": "llm", "id": "step-2", "instructions": "Write a summary from the facts above" }
]
}| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique step identifier. |
steps | WorkflowNode[] | required | Ordered list of child nodes (min 1). |
fork
Run multiple paths concurrently with configurable merge strategy.
{
"kind": "fork",
"id": "parallel-search",
"mode": "all",
"paths": [
{ "kind": "llm", "id": "search-a", "instructions": "Search topic A" },
{ "kind": "llm", "id": "search-b", "instructions": "Search topic B" }
],
"merge": "concat",
"concurrency": 3
}| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique step identifier. |
mode | 'all' | 'race' | 'settle' | required | all waits for every path; race returns the first; settle waits for all but tolerates failures. |
paths | WorkflowNode[] | required | Child nodes to execute concurrently. |
merge | 'last' | 'first' | 'concat' | 'last' | How to combine results. Ignored in race mode. |
concurrency | number | undefined | Maximum concurrent paths. |
branch
Route input to one of several targets based on substring matching.
{
"kind": "branch",
"id": "router",
"routes": [
{ "match": "code", "target": { "kind": "llm", "id": "code-path", "instructions": "Write code" } },
{ "match": "explain", "target": { "kind": "llm", "id": "explain-path", "instructions": "Explain the concept" } }
],
"default": { "kind": "llm", "id": "fallback", "instructions": "Handle the request" }
}| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique step identifier. |
routes | { match: string, target: WorkflowNode }[] | required | Ordered matching rules. First match wins (case-insensitive substring). |
default | WorkflowNode | undefined | Fallback node when no route matches. |
loop
Repeat a body node until a predicate is satisfied.
{
"kind": "loop",
"id": "refine",
"body": { "kind": "llm", "id": "refine-step", "instructions": "Improve the draft" },
"until": { "kind": "maxSteps", "n": 5 },
"maxIterations": 10
}| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique step identifier. |
body | WorkflowNode | required | The node to repeat. |
until | UntilPredicate | required | Termination condition. See Until Predicates. |
maxIterations | number | undefined | Hard cap on iterations (safety guard). |
spawn
Run a child node in an isolated child context.
{
"kind": "spawn",
"id": "background-task",
"child": { "kind": "llm", "id": "worker", "instructions": "Process in background" },
"timeout": 30000
}| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique step identifier. |
child | WorkflowNode | required | The node to execute in a child context. |
timeout | number | undefined | Timeout in milliseconds. |
provide
Wrap a child node with memory layers.
{
"kind": "provide",
"id": "with-memory",
"child": { "kind": "llm", "id": "agent", "instructions": "..." },
"layers": ["conversation-summary"]
}| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique step identifier. |
child | WorkflowNode | required | The node to wrap. |
layers | string[] | required | Layer names. Currently layers are referenced by name but not resolved from a registry -- the harness's default layers are inherited instead. |
every
Execute a step on a fixed interval (periodic scheduling).
{
"kind": "every",
"id": "heartbeat",
"step": { "kind": "llm", "id": "check", "instructions": "Check system status" },
"ms": 60000,
"onError": "continue"
}| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique step identifier. |
step | WorkflowNode | required | The node to execute each interval. |
ms | number | required | Interval in milliseconds. |
onError | 'continue' | 'fail' | undefined | Whether to keep running or abort on error. |
Until Predicates
Loop termination conditions are expressed as named predicates. Each predicate is a JSON object with a kind discriminant. See Loop & Until for the programmatic equivalents.
Named predicates
| Kind | Fields | Description |
|---|---|---|
maxSteps | n: number | Stop after n iterations. |
maxCost | usd: number | Stop when cumulative cost exceeds usd dollars. |
maxDuration | ms: number | Stop after ms milliseconds. |
noToolCalls | -- | Stop when the LLM produces no tool calls in a round. |
outputContains | marker: string | Stop when output contains the marker substring. |
outputEquals | sentinel: string | Stop when output exactly equals the sentinel. |
converged | threshold?: number | Stop when output similarity between rounds exceeds threshold (0-1, defaults to the builder default). |
Combinators
Combine multiple predicates with any (logical OR) or all (logical AND):
{
"kind": "any",
"predicates": [
{ "kind": "maxSteps", "n": 10 },
{ "kind": "outputContains", "marker": "DONE" }
]
}{
"kind": "all",
"predicates": [
{ "kind": "maxSteps", "n": 3 },
{ "kind": "noToolCalls" }
]
}dynamicWorkflow()
The dynamicWorkflow pattern hands the planning to an LLM: it generates a workflow document as JSON, validates it, hydrates it, and executes it -- all within a single harness run. If validation fails, the planner retries with error feedback up to maxRevisions times.
import { dynamicWorkflow, AgentHarness, type Tool } from '@noetic-tools/core';
declare const searchTool: Tool;
declare const calcTool: Tool;
const agent = dynamicWorkflow({
model: 'openai/gpt-4o',
tools: [searchTool, calcTool],
instructions: 'Plan the most efficient workflow',
maxDepth: 5,
});
const harness = new AgentHarness({
name: 'planner',
initialStep: agent,
params: {},
});
await harness.execute('Research and summarize quantum computing advances');
const response = await harness.getAgentResponse();DynamicWorkflowOpts
| Field | Type | Default | Description |
|---|---|---|---|
model | string | 'openai/gpt-4o' | Model for the planner LLM. |
instructions | string | undefined | Additional instructions prepended to the planner prompt. |
tools | Tool[] | required | Tools the generated workflow may reference by name. |
maxDepth | number | 5 | Maximum allowed tree depth for generated workflows. |
maxRevisions | number | 3 | Retries with error feedback on validation failure. |
Throws NoeticConfigError with code WORKFLOW_VALIDATION_FAILED if the planner cannot produce a valid workflow within the revision limit.
parseAndRunWorkflow()
For pre-built JSON workflows -- parse, validate, hydrate, and execute in one call.
import { parseAndRunWorkflow, AgentHarness, type Tool } from '@noetic-tools/core';
declare const workflowDocument: unknown;
declare const searchTool: Tool;
declare const calcTool: Tool;
const harness = new AgentHarness({ name: 'runner', params: {} });
const ctx = harness.createContext();
const result = await parseAndRunWorkflow({
json: workflowDocument,
harness,
ctx,
tools: [searchTool, calcTool],
maxDepth: 5,
});ParseAndRunWorkflowOpts
| Field | Type | Default | Description |
|---|---|---|---|
json | unknown | required | Raw JSON (string or parsed object) representing a workflow document. |
harness | AgentHarnessContract | required | The AgentHarness to execute the workflow with. |
ctx | Context | required | Execution context. |
tools | Tool[] | required | Available tools the workflow may reference by name. |
maxDepth | number | 5 | Maximum allowed workflow tree depth. |
Returns Promise<string> -- the string output of the executed workflow.
Throws NoeticConfigError with code WORKFLOW_VALIDATION_FAILED if the JSON does not match WorkflowDocumentSchema. Throws NoeticConfigError with code UNKNOWN_TOOL_REFERENCE if a tool name cannot be resolved.
Hydration API
For lower-level control, use hydrateWorkflow and hydrateNode to convert JSON workflow structures into live Step objects without immediately executing them.
import { hydrateWorkflow, hydrateNode } from '@noetic-tools/core';
import type { HydrationContext } from '@noetic-tools/core';
const hydrationCtx: HydrationContext = {
tools: new Map(myTools.map((t) => [t.name, t])),
executeStep: harness.run.bind(harness),
};
// Hydrate an entire document
const rootStep = hydrateWorkflow(workflowDoc, hydrationCtx);
// Or hydrate a single node
const singleStep = hydrateNode(someNode, hydrationCtx);
// Then execute manually
const result = await harness.run(rootStep, 'input text', ctx);HydrationContext
| Field | Type | Description |
|---|---|---|
tools | ReadonlyMap<string, Tool> | Tool registry mapping tool names to Tool instances. |
executeStep | ExecuteStepFn | Step executor function (typically harness.run.bind(harness)). |
hydrateWorkflow(doc, ctx) converts a validated WorkflowDocument into its root Step. hydrateNode(node, ctx) converts a single WorkflowNode. Both return Step<ContextMemory, string, string> -- indistinguishable from programmatically built steps.
Throws NoeticConfigError with code UNKNOWN_NODE_KIND if a node kind is unrecognised, UNKNOWN_TOOL_REFERENCE if a tool name cannot be resolved, or UNKNOWN_UNTIL_PREDICATE if an until predicate kind is unrecognised.
Limitations
- No
step.runnode --runsteps carry arbitrary closures, which are not JSON-serialisable. Usetoolnodes to invoke registered tools instead. - Tools resolved by name -- every tool referenced in the workflow must be passed in the
toolsarray. The hydrator looks them up bytool.name. - Provide layers not resolved from registry --
providenodes reference layers by name, but the hydrator does not currently resolve them from a layer registry. The harness's default memory layers are inherited instead. - Branch routing is substring-based -- branch routes use case-insensitive substring matching on the input string. Complex routing logic requires programmatic
branch()steps.
Related Pages
- AgentHarness -- the execution engine that runs hydrated workflows.
- Steps -- the step primitives that JSON nodes hydrate into.
- Loop & Until -- programmatic equivalents of JSON until predicates.
- Fork -- the fork operator that
forknodes hydrate into. - Spawn -- isolated child execution for
spawnnodes.