Skip to main content

Your First Trace

An execution graph is the central data structure in AgentFlow. It records every node (agent, tool, subagent, decision) that ran during a single agent execution, the parent-child relationships between them, and the lifecycle status of each node. This page walks through building one from scratch.

Build a simple graph

import { createGraphBuilder, getStats } from 'agentflow-core';

// 1. Create a builder for this agent run
const builder = createGraphBuilder({
agentId: 'research-agent',
trigger: 'user-request',
});

// 2. Start the root node — the top-level agent
const rootId = builder.startNode({ type: 'agent', name: 'main' });

// 3. Start child nodes, passing the parent's ID
const searchId = builder.startNode({ type: 'tool', name: 'web-search', parentId: rootId });
builder.endNode(searchId); // completed successfully

const llmId = builder.startNode({ type: 'agent', name: 'summarise', parentId: rootId });
builder.endNode(llmId);

// 4. Close the root node and freeze the graph
builder.endNode(rootId);
const graph = builder.build();

console.log(getStats(graph));

getStats returns a GraphStats object:

{
totalNodes: 3,
byStatus: { completed: 3, running: 0, failed: 0, hung: 0, timeout: 0 },
byType: { agent: 2, tool: 1, subagent: 0, wait: 0, decision: 0, custom: 0 },
depth: 1,
duration: 12, // milliseconds
failureCount: 0,
hungCount: 0
}

Node types

TypeWhen to use
agentA reasoning step — the top-level agent or an LLM call
toolA tool call (API, database query, shell command)
subagentA spawned child agent running in a separate process or thread
decisionA branching point (e.g. router deciding which path to take)
waitA pause waiting on an external event or human input
customAnything else you want to track

Node statuses

Nodes start in running and transition to one of:

StatusSet by
completedbuilder.endNode(id) — default
failedbuilder.failNode(id, error) or builder.endNode(id, 'failed')
hungbuilder.endNode(id, 'hung') — for manually detected hangs
timeoutbuilder.endNode(id, 'timeout')

If any node ends in failed, hung, or timeout, the whole graph's status becomes 'failed'.

Recording a failure

const fetchId = builder.startNode({ type: 'tool', name: 'fetch-prices', parentId: rootId });

try {
// ... your tool logic ...
builder.endNode(fetchId);
} catch (err) {
builder.failNode(fetchId, err as Error);
// node.metadata.error is set automatically
}

Attaching metadata

Pass a metadata object when starting a node to record arbitrary context:

const toolId = builder.startNode({
type: 'tool',
name: 'llm-call',
parentId: rootId,
metadata: { model: 'claude-3-5-sonnet', tokens: 1024 },
});

You can also update state mid-execution:

builder.updateState(toolId, { tokensUsed: 847, cached: false });

The graph object

builder.build() returns a frozen ExecutionGraph:

interface ExecutionGraph {
readonly id: string; // unique graph ID
readonly agentId: string; // from config
readonly trigger: string; // from config
readonly rootNodeId: string; // ID of the first node
readonly nodes: ReadonlyMap<string, ExecutionNode>; // all nodes by ID
readonly edges: readonly ExecutionEdge[]; // parent→child relationships
readonly startTime: number; // epoch ms
readonly endTime: number | null; // null if still running
readonly status: 'running' | 'completed' | 'failed';
readonly events: readonly TraceEvent[]; // full ordered event log
}

Edges are created automatically when you pass parentId to startNode. The default edge type is 'spawned'. You can add explicit edges with builder.addEdge(from, to, type) using types 'spawned', 'waited_on', 'called', 'retried', or 'branched'.

Using withParent for nested scopes

If you have deeply nested code, withParent lets child startNode calls inherit a parent automatically:

builder.withParent(rootId, () => {
// parentId is implicit here
const toolId = builder.startNode({ type: 'tool', name: 'search' });
builder.endNode(toolId);
});

Next: Process mining — discover patterns, variants, and bottlenecks across multiple runs.