NOETIC
Framework

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" }
}
FieldTypeDescription
version1Schema version. Currently only 1 is supported.
rootWorkflowNodeThe 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 }
}
FieldTypeDefaultDescription
idstringrequiredUnique step identifier.
modelstring'openai/gpt-4o'Model identifier (OpenRouter format).
instructionsstringrequiredSystem/user prompt for the LLM.
toolsstring[]undefinedTool names resolved from the tool registry.
paramsobjectundefinedModel parameters: temperature, topP, maxTokens, stopSequences.

tool

Invoke a registered tool directly.

{
  "kind": "tool",
  "id": "calc-step",
  "toolName": "calculator",
  "args": { "expression": "2 + 2" }
}
FieldTypeDefaultDescription
idstringrequiredUnique step identifier.
toolNamestringrequiredName of a tool in the tool registry.
argsRecord<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" }
  ]
}
FieldTypeDefaultDescription
idstringrequiredUnique step identifier.
stepsWorkflowNode[]requiredOrdered 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
}
FieldTypeDefaultDescription
idstringrequiredUnique step identifier.
mode'all' | 'race' | 'settle'requiredall waits for every path; race returns the first; settle waits for all but tolerates failures.
pathsWorkflowNode[]requiredChild nodes to execute concurrently.
merge'last' | 'first' | 'concat''last'How to combine results. Ignored in race mode.
concurrencynumberundefinedMaximum 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" }
}
FieldTypeDefaultDescription
idstringrequiredUnique step identifier.
routes{ match: string, target: WorkflowNode }[]requiredOrdered matching rules. First match wins (case-insensitive substring).
defaultWorkflowNodeundefinedFallback 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
}
FieldTypeDefaultDescription
idstringrequiredUnique step identifier.
bodyWorkflowNoderequiredThe node to repeat.
untilUntilPredicaterequiredTermination condition. See Until Predicates.
maxIterationsnumberundefinedHard 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
}
FieldTypeDefaultDescription
idstringrequiredUnique step identifier.
childWorkflowNoderequiredThe node to execute in a child context.
timeoutnumberundefinedTimeout in milliseconds.

provide

Wrap a child node with memory layers.

{
  "kind": "provide",
  "id": "with-memory",
  "child": { "kind": "llm", "id": "agent", "instructions": "..." },
  "layers": ["conversation-summary"]
}
FieldTypeDefaultDescription
idstringrequiredUnique step identifier.
childWorkflowNoderequiredThe node to wrap.
layersstring[]requiredLayer 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"
}
FieldTypeDefaultDescription
idstringrequiredUnique step identifier.
stepWorkflowNoderequiredThe node to execute each interval.
msnumberrequiredInterval in milliseconds.
onError'continue' | 'fail'undefinedWhether 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

KindFieldsDescription
maxStepsn: numberStop after n iterations.
maxCostusd: numberStop when cumulative cost exceeds usd dollars.
maxDurationms: numberStop after ms milliseconds.
noToolCalls--Stop when the LLM produces no tool calls in a round.
outputContainsmarker: stringStop when output contains the marker substring.
outputEqualssentinel: stringStop when output exactly equals the sentinel.
convergedthreshold?: numberStop 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

FieldTypeDefaultDescription
modelstring'openai/gpt-4o'Model for the planner LLM.
instructionsstringundefinedAdditional instructions prepended to the planner prompt.
toolsTool[]requiredTools the generated workflow may reference by name.
maxDepthnumber5Maximum allowed tree depth for generated workflows.
maxRevisionsnumber3Retries 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

FieldTypeDefaultDescription
jsonunknownrequiredRaw JSON (string or parsed object) representing a workflow document.
harnessAgentHarnessContractrequiredThe AgentHarness to execute the workflow with.
ctxContextrequiredExecution context.
toolsTool[]requiredAvailable tools the workflow may reference by name.
maxDepthnumber5Maximum 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

FieldTypeDescription
toolsReadonlyMap<string, Tool>Tool registry mapping tool names to Tool instances.
executeStepExecuteStepFnStep 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.run node -- run steps carry arbitrary closures, which are not JSON-serialisable. Use tool nodes to invoke registered tools instead.
  • Tools resolved by name -- every tool referenced in the workflow must be passed in the tools array. The hydrator looks them up by tool.name.
  • Provide layers not resolved from registry -- provide nodes 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.
  • 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 fork nodes hydrate into.
  • Spawn -- isolated child execution for spawn nodes.

On this page