Core Concepts
Understand agents, nodes, state, and durability in JamJet.
Core Concepts
JamJet is a durable runtime for AI agents. The key idea is simple: workflows are executed as recoverable state transitions, not ephemeral function calls. That is what makes replay, crash recovery, approval pauses, and exact traces possible. This page covers the key primitives.
Choosing an authoring approach
JamJet has three ways to build agents. All three compile to the same IR and run on the same Rust runtime — they differ in when to reach for each one.
Decision flowchart
Is your agent calling LLMs with reasoning strategies
(react, plan-and-execute, critic)?
│
├─ YES → Use the Agent + Workflow SDK
│ (from jamjet import Agent, Workflow, tool)
│
└─ NO → Is the flow mostly tool orchestration, routing,
or config you want to change without code deploys?
│
├─ YES → Use YAML workflows
│
└─ NO → Use the Python decorator API
(@workflow, @node)The three approaches
| Approach | Best for | State | Example |
|---|---|---|---|
| Agent + Workflow SDK | Multi-agent systems with reasoning | Pydantic BaseModel (typed, validated) | claims-processing, wealth-management |
| YAML workflows | Tool pipelines, config-driven flows | Schema in YAML (typed) | rag-assistant |
| Python decorator API | Custom logic, Python library access | Dict-based State | Python SDK docs |
Agent + Workflow SDK
Use this when your agents need to think — choose tools, reason about results, refine outputs. Each agent gets a reasoning strategy suited to its task.
from jamjet import Agent, Workflow, tool
from pydantic import BaseModel
@tool
def search_web(query: str) -> dict:
"""Search the web for information."""
...
researcher = Agent(
name="researcher",
model="claude-sonnet-4-6",
tools=[search_web],
instructions="Find accurate information on the given topic.",
strategy="react", # tight tool-use loop
max_iterations=5,
)
writer = Agent(
name="writer",
model="claude-sonnet-4-6",
instructions="Write a clear summary from the research.",
strategy="critic", # draft → evaluate → refine
max_iterations=3,
)
workflow = Workflow("research-writer", version="0.1.0")
@workflow.state
class ResearchState(BaseModel):
topic: str
research: str | None = None
summary: str | None = None
@workflow.step
async def research(state: ResearchState) -> ResearchState:
result = await researcher.run(f"Research: {state.topic}")
return state.model_copy(update={"research": result.output})
@workflow.step
async def write(state: ResearchState) -> ResearchState:
result = await writer.run(f"Summarize:\n{state.research}")
return state.model_copy(update={"summary": result.output})Choose this when: agents need strategies (react, plan-and-execute, critic, consensus, debate), you want typed Pydantic state, you need human-in-the-loop (human_approval=True), or multiple specialist agents collaborate.
YAML workflows
Use this when the flow is deterministic orchestration — call this tool, then that model, route based on a condition. No agent reasoning needed.
id: summarize-url
version: "0.1.0"
nodes:
fetch:
type: tool
server: web-fetcher
tool: fetch_page
arguments:
url: "{{ state.url }}"
output_key: page_content
next: summarize
summarize:
type: model
model: claude-haiku-4-5-20251001
prompt: "Summarize: {{ state.page_content }}"
output_key: summaryChoose this when: the flow is a fixed pipeline of tools and models, you want non-developers to edit the flow, or you need to change routing without redeploying code.
Python decorator API
Use this when you need custom Python logic inside nodes — data transforms, library calls, conditional logic that is too complex for YAML.
from jamjet import workflow, node, State
@workflow(id="data-processor", version="0.1.0")
class DataProcessor:
@node(start=True)
async def process(self, state: State) -> State:
import pandas as pd
df = pd.read_csv(state["file_path"])
summary = df.describe().to_dict()
return {"analysis": summary}
@node
async def report(self, state: State) -> State:
response = await self.model(
model="claude-sonnet-4-6",
prompt=f"Write a report from this analysis:\n{state['analysis']}",
)
return {"report": response.text}Choose this when: you need Python libraries inside nodes, the logic is too complex for YAML but does not need agent reasoning, or you prefer class-based workflows.
Can I mix them?
Yes. All three compile to the same IR. A YAML workflow can call an agent-as-tool. A Python workflow step can invoke a YAML-defined sub-workflow. The runtime does not care which authoring surface produced the IR.
Workflows
A workflow is a directed graph of nodes. It has:
- A unique
idandversion - A
state_schema— the typed shape of data flowing through the graph - A
startnode where execution begins - One or more
endnodes
workflow:
id: my-agent
version: 0.1.0
state_schema:
query: str
answer: str
confidence: float
start: thinkWorkflows are compiled to an IR (Intermediate Representation) graph before execution. The IR is what the Rust scheduler actually runs — YAML and Python are just authoring surfaces.
Nodes
Nodes are the units of computation in a workflow. Each node has a type that determines what it does:
| Node type | What it does |
|---|---|
model | Calls an LLM (Claude, GPT-4, Gemini, etc.) |
tool | Calls an external tool via MCP |
http | Makes an HTTP request |
branch | Routes execution based on a condition |
parallel | Fans out to multiple branches simultaneously |
wait | Pauses until an external event |
eval | Scores output quality (rubric, assertion, latency) |
end | Terminates the workflow |
Every node reads from and writes to state.
State
State is the shared data store for a workflow execution. It persists across nodes and across restarts.
state_schema:
query: str # input from the user
search_results: list[str] # intermediate data
answer: str # final outputState is typed — the schema is validated at compile time. At runtime, each node can read any state key and write to its output_key.
tip: State is stored in the database, not in memory. If the runtime crashes mid-execution, the state is fully recovered and execution resumes from the last checkpoint.
Executions
An execution is a single run of a workflow with a specific input. Each execution gets a unique ID (e.g., exec_01JM4X8NKWP2).
Executions are:
- Durable — stored in the database, survive restarts
- Observable — every state transition is recorded as an event
- Inspectable — view full state, event timeline, and token usage with
jamjet inspect
Durability
Durability is JamJet's core guarantee: executions always complete, even if the runtime crashes.
This works through event sourcing:
- Before each node runs, a
node_startedevent is written to the database - After each node completes, a
node_completedevent is written with the state patch - On restart, the scheduler replays the event log to reconstruct exactly where execution stopped
- Execution resumes from the first incomplete node
No work is lost. No node runs twice.
note: This is different from "at-least-once" delivery. JamJet's scheduler uses distributed locks to ensure each node runs exactly once, even with multiple worker processes.
Agents
An agent is a workflow that can:
- Be discovered and called by other agents (via Agent Cards)
- Delegate tasks to other agents (via A2A protocol)
- Maintain long-running state across multiple user interactions
Every agent has an Agent Card — a machine-readable description of its capabilities, endpoints, and input/output schema. This is the foundation for the A2A protocol.
The Scheduler
The JamJet scheduler is written in Rust and runs as part of jamjet dev (locally) or the hosted runtime (in production).
It:
- Polls the execution queue for pending work
- Acquires a lock on the execution to prevent duplicate runs
- Dispatches nodes to worker threads
- Writes checkpoints after each node
The scheduler is the reason JamJet workflows are durable by default — it never forgets an execution.
Local vs. Production
| Feature | jamjet dev (local) | Hosted / self-hosted |
|---|---|---|
| Storage | SQLite | PostgreSQL |
| Workers | Single process | Distributed |
| MCP servers | Local stdio | Remote SSE/HTTP |
| Auth | None | mTLS / API keys |
The programming model is identical — the same YAML or Python code runs unchanged in both environments.
Next steps
- Quickstart — build your first durable agent
- Python SDK — decorators, routing, parallel steps
- Try crash recovery — see durability in action