Skip to content

The 6 node kinds

OpenExpertise ships six node kinds. Picking the right one for each step is most of what experience design looks like.

At a glance

KindUse it when…Backed by
toolThe step is deterministic code — fetch, parse, transform, scoreYour .mjs file
agentThe step needs LLM judgment with a structured outputAnthropic / OpenAI client
skillYou have a reusable, version-tagged SKILL.md packageSkillDispatcher + LLM client
datasetThe step loads tabular or document data into statefile / SQLite / HTTP / MCP-resource
experienceThe step is itself a whole sub-experience (graph of graphs)Nested runExperience()
cli-agentThe step needs the full power of Claude Code / Codex / GeminiSubprocess + JSON-mode parser

The decision tree

Is this step deterministic? (no judgment needed)

   Yes ──→ TOOL
   No

Is the output a single, well-shaped JSON object I can specify?

   Yes ──→ AGENT  (one LLM call, AJV-validated)
   No

Does it need file system, search, MCP, or other tool ecosystem the LLM should use?

   Yes ──→ CLI-AGENT  (delegate to Claude Code / Codex / Gemini)
   No

Is it a packaged, reusable routine I already have as SKILL.md?

   Yes ──→ SKILL
   No

Does it produce a row-set I want loaded as state?

   Yes ──→ DATASET
   No

Is it a whole sub-flow I want to invoke atomically?

   Yes ──→ EXPERIENCE

What every node has in common

Regardless of kind, every node spec has:

yaml
- id: my_node # unique within the graph
  kind: tool # one of the 6
  phase: collect # optional, for UI grouping
  reads: [foo, bar] # state fields injected into the node's input
  writes: [baz] # state fields the node is allowed to write to
  on_error: # optional retry/skip/fail policy
    policy: retry
    attempts: 3
    backoff: exponential
    base_ms: 1000
  for_each: # optional fan-out
    source: $.dimensions
    concurrency: 4
  # ... plus kind-specific fields ...

This common shape is the contract every dispatcher honors. The kind-specific fields go below.

Composition rules

  • Reads ⊆ state.schema. A node can only declare reads for fields that exist in state.schema. Validator-enforced.
  • Writes ⊆ state.schema. Same.
  • State fields can have merge strategies. Multiple writers to the same field combine via array_append / set_once / last_wins. See State.
  • No cycles. Edges must form a DAG. Validator-enforced.
  • Phases are cosmetic. They group nodes in the TUI and in event logs; they don't affect runtime ordering.

The full grammar per kind

Each kind has its own deep-dive page with required fields, examples, and gotchas:

  • toolimpl: ./tools/<name>.mjs
  • agentprompt: ./prompts/<name>.md + inline schema
  • skillimpl: ./skills/<name> (SKILL.md dir)
  • datasetsource: { type, uri, query?, format? }
  • experienceimpl: ./sub-experience + state_scope: isolated|shared
  • cli-agentprovider: claude-code|codex|gemini + inline prompt

Real-world combinations

Looking at the examples library, here's the mix-and-match:

ExampleTools used
hello-tooltool
agent-echoagent
dataset-aggregatedataset + tool
review-branchtool + agent ×3
oncall-runbooktool + agent (with for_each)
issue-triagetool + agent ×4 (with when: edges)
release-gatestool ×3 + cli-agent + agent
cli-orchestrationcli-agent ×2
tri-cli-orchestrationcli-agent ×3 (3 vendors, one graph)
deep-researchtool + agent ×4
systematic-debuggingtool + agent ×3

→ Start with the tool page — it's the simplest.

Released under the MIT License.