JamJet

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

ApproachBest forStateExample
Agent + Workflow SDKMulti-agent systems with reasoningPydantic BaseModel (typed, validated)claims-processing, wealth-management
YAML workflowsTool pipelines, config-driven flowsSchema in YAML (typed)rag-assistant
Python decorator APICustom logic, Python library accessDict-based StatePython 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: summary

Choose 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 id and version
  • A state_schema — the typed shape of data flowing through the graph
  • A start node where execution begins
  • One or more end nodes
workflow:
  id: my-agent
  version: 0.1.0
  state_schema:
    query: str
    answer: str
    confidence: float
  start: think

Workflows 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 typeWhat it does
modelCalls an LLM (Claude, GPT-4, Gemini, etc.)
toolCalls an external tool via MCP
httpMakes an HTTP request
branchRoutes execution based on a condition
parallelFans out to multiple branches simultaneously
waitPauses until an external event
evalScores output quality (rubric, assertion, latency)
endTerminates 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 output

State 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:

  1. Before each node runs, a node_started event is written to the database
  2. After each node completes, a node_completed event is written with the state patch
  3. On restart, the scheduler replays the event log to reconstruct exactly where execution stopped
  4. 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:

  1. Polls the execution queue for pending work
  2. Acquires a lock on the execution to prevent duplicate runs
  3. Dispatches nodes to worker threads
  4. Writes checkpoints after each node

The scheduler is the reason JamJet workflows are durable by default — it never forgets an execution.

Local vs. Production

Featurejamjet dev (local)Hosted / self-hosted
StorageSQLitePostgreSQL
WorkersSingle processDistributed
MCP serversLocal stdioRemote SSE/HTTP
AuthNonemTLS / API keys

The programming model is identical — the same YAML or Python code runs unchanged in both environments.


Next steps

On this page