Multi-Agent Research Network with Inngest
Build a durable multi-agent system where specialists collaborate via shared state.
Introduction
30 Second Summary
When you ask a single AI to research a topic, write code, and fact-check itself, quality drops fast. Teams of specialists outperform generalists because each member focuses on what they do best.
In this project, you will build a multi-agent research network where specialized AI agents collaborate through shared state. A Web Researcher fetches live content, a Code Analyzer reads local files, and a Summarizer synthesizes everything into a final report.
What You'll Build
You type a research question, hit invoke, and watch the Inngest Dev Server trace light up as three agents take turns fetching web pages, analyzing code, and writing a combined brief.
By the end of this project, you'll have:
- A working multi-agent network where you ask a research question and watch three specialized agents collaborate to produce a structured answer.
- A real-time execution trace in the Inngest Dev Server showing which agent ran, what tools it called, and the state flowing between them.
- A durable execution demonstration where you intentionally break a tool, watch only the failed step retry, and see all completed work preserved.
- Secret Mission: Add a Fact Checker agent that validates claims from the other agents before the final summary.
Are there any prerequisites?
You need Node.js installed, an OpenRouter account with a free API key, and basic familiarity with TypeScript. Step 1 walks you through everything else.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters. You're building a multi-agent research network where specialized AI agents collaborate through shared state, and durable orchestration ensures that when one agent's tool fails, only the failed step retries while all completed work is preserved.
Set Up the Project
Before any agent can research, analyze, or summarize, it needs three things: a framework to run inside, a connection to a language model, and a way to observe what it does.
In this step, you'll scaffold the project, connect to a free LLM through OpenRouter, and verify that the Inngest Dev Server can see your agent.
In this step, get ready to:
- Initialize the project with AgentKit and its dependencies.
- Create a test agent connected to a free LLM via OpenRouter.
- Start the AgentKit server and the Inngest Dev Server to confirm everything is wired up.
Initialize the project and install dependencies
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir research-network
cd research-network
- Initialize a new Node.js project by running:
npm init -y
You should see output showing a new package.json file was created in your research-network folder.
- Install the project dependencies by running:
npm install @inngest/agent-kit inngest zod
What are these packages?
- @inngest/agent-kit is the framework for building multi-agent networks with tools and routing.
- inngest is the peer dependency that provides durable execution, step-level retries, and the Dev Server connection.
- zod defines typed parameter schemas for agent tools.
You should see output confirming all three packages were added to node_modules.
Seeing install errors?
Make sure you're inside the research-network folder (check with pwd) and that your Node.js version is 18 or higher.
Help me fix npm install errors for @inngest/agent-kit
- Open your research-network folder in your code editor.
- Create a new file called tsconfig.json in the root of your research-network folder.
- Paste the following TypeScript configuration into tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["*.ts"]
}
What does this config do?
This tells TypeScript to target modern JavaScript (ES2022) and use ESNext modules. The bundler module resolution matches how tsx resolves imports when you run the project shortly.
- Save tsconfig.json.
- Confirm the file exists by running this in your terminal:
ls
You should see node_modules, package.json, package-lock.json, and tsconfig.json listed.
Create the test agent
Now you'll write a minimal agent that connects to a free LLM through OpenRouter's API. This confirms the framework can reach the model before you build anything complex.
- Create a new file called index.ts in the root of your research-network folder.
- Paste the following code into index.ts:
import { createAgent, openai } from "@inngest/agent-kit";
import { createServer } from "@inngest/agent-kit/server";
// Configure OpenRouter as the model provider
const model = openai({
model: "nvidia/nemotron-3-ultra-550b-a55b:free",
baseUrl: "https://openrouter.ai/api/v1",
apiKey: process.env.OPENAI_API_KEY,
});
// Create a simple test agent
const testAgent = createAgent({
name: "Test Agent",
description: "A simple test agent",
system: "You are a helpful assistant.",
model,
});
// Start the server and register the agent with Inngest
const server = createServer({
agents: [testAgent],
});
server.listen(3000, () => console.log("AgentKit server running on port 3000!"));
What does this code do?
- openai({ baseUrl: "https://openrouter.ai/api/v1" }) uses AgentKit's OpenAI-compatible adapter pointed at OpenRouter instead of OpenAI directly.
- createAgent defines an agent with a name, description, system prompt, and model. This is the smallest possible agent.
- createServer spins up an HTTP server that automatically registers your agents with the Inngest Dev Server so you can invoke and observe them.
- Save index.ts.
✔️ Awesome, I've got everything!
Make sure both tsconfig.json and index.ts are saved before continuing.
ⓧ I'd like to double check the full code
Here is the full index.ts for comparison:
import { createAgent, openai } from "@inngest/agent-kit";
import { createServer } from "@inngest/agent-kit/server";
const model = openai({
model: "nvidia/nemotron-3-ultra-550b-a55b:free",
baseUrl: "https://openrouter.ai/api/v1",
apiKey: process.env.OPENAI_API_KEY,
});
const testAgent = createAgent({
name: "Test Agent",
description: "A simple test agent",
system: "You are a helpful assistant.",
model,
});
const server = createServer({
agents: [testAgent],
});
server.listen(3000, () => console.log("AgentKit server running on port 3000!"));
And here is tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["*.ts"]
}
Start the servers and verify registration
Your agent needs two processes running: the AgentKit server (which hosts your agents) and the Inngest Dev Server (which orchestrates and traces execution).
- In your terminal, set the OPENAI_API_KEY environment variable to your OpenRouter API key.
macOS
- Run this command, replacing the placeholder with your actual key:
export OPENAI_API_KEY=your-openrouter-api-key-here
Windows
- Run this command, replacing the placeholder with your actual key:
set OPENAI_API_KEY=your-openrouter-api-key-here
Where do I find my OpenRouter API key?
Go to openrouter.ai/keys and copy your key. AgentKit's openai adapter reads OPENAI_API_KEY by default, which is why you use that variable name even though this is OpenRouter.
Before you start the server, what do you think you'll see if the connection to OpenRouter is configured correctly?
- Start the AgentKit server by running:
npx tsx ./index.ts
You should see AgentKit server running on port 3000! printed in the terminal.
Seeing an error instead?
If you see a module resolution error, check that tsconfig.json has "moduleResolution": "bundler" set. If you see a permissions error, make sure you're in the research-network folder.
Help me fix this AgentKit server startup error
- Leave that terminal running. Open a second terminal window.
- In the second terminal, navigate back to your project folder by running:
cd ~/Desktop/research-network
- Start the Inngest Dev Server by running:
npx inngest-cli@latest dev -u http://localhost:3000/api/inngest
What does this command do?
The Inngest Dev Server connects to your AgentKit server at the URL you pass with -u. It discovers your registered agents, provides a web UI for invoking them, and shows a full trace of every step, tool call, and retry.
You should see output confirming the Dev Server started and discovered your agent function.
- Open http://localhost:8288/functions in your browser.
You should see your Test Agent listed on the functions page. This confirms the AgentKit server, the Inngest Dev Server, and the model configuration are all connected.
Don't see your agent listed?
Make sure the first terminal (AgentKit server) is still running and showing the AgentKit server running on port 3000! message. The Inngest Dev Server needs an active connection to discover agents.
Also confirm the URL in your second terminal command matches port 3000.
Help me connect the Inngest Dev Server to my AgentKit server
Your development environment is ready and your test agent is registered. Next up, you'll give the Web Researcher agent a real superpower: a tool that fetches live web content.
Build the Web Researcher Agent with a Fetch Tool
Your test agent from Step 1 can chat, but it cannot do anything. It has no way to gather information from the real world.
In this step, you will give an agent a real capability: fetching live web content. This is how tool calling works. The LLM decides when to invoke the tool, your code executes it, and the result feeds back into the agent's reasoning.
In this step, get ready to:
- Build a fetch_webpage tool that retrieves and cleans live web content.
- Create a Web Researcher agent that knows how to use the tool and save its findings.
- Wire the agent into a network and invoke it from the Inngest Dev Server.
Create the fetch_webpage tool
The fetch_webpage tool gives the agent the power to reach out to any URL and pull back its text content. You will rebuild index.ts from scratch with new imports, a typed state interface, and the tool definition.
- In index.ts, select all the existing code and delete it.
- Paste the following updated imports, model configuration, and state interface at the top of the empty file:
import {
createAgent,
createNetwork,
createTool,
openai,
} from "@inngest/agent-kit";
import { createServer } from "@inngest/agent-kit/server";
import { z } from "zod";
// Configure OpenRouter as the model provider
const model = openai({
model: "nvidia/nemotron-3-ultra-550b-a55b:free",
baseUrl: "https://openrouter.ai/api/v1",
apiKey: process.env.OPENAI_API_KEY,
});
// Define the network state interface
interface ResearchState {
web_research?: string;
}
What does this code do?
The imports now include createNetwork, createTool, and z from Zod. These are the building blocks for giving agents tools with validated parameters.
The ResearchState interface defines the shape of shared state that flows between agents. For now it only holds web_research. You will add more fields as the network grows.
- Below the ResearchState interface, add the fetch_webpage tool by pasting this code:
const fetchWebpageTool = createTool({
name: "fetch_webpage",
description: "Fetch the text content of a webpage given its URL",
parameters: z.object({
url: z.string().describe("The URL to fetch"),
}),
handler: async ({ url }) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status}`);
}
const html = await response.text();
// Strip HTML tags and collapse whitespace
const text = html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
return text.slice(0, 3000);
},
});
What does fetchWebpageTool do?
The parameters field uses a Zod schema. The LLM reads the description and decides when to call the tool. AgentKit validates the input automatically.
The handler fetches the URL with Node's built-in fetch, strips all HTML tags using a regex, normalizes whitespace, and truncates to 3000 characters. If the fetch fails, it throws an error so Inngest's retry mechanism can handle it.
- Save index.ts.
- Switch to the terminal where your AgentKit server is running and stop it with Ctrl+C.
- Verify the code compiles without errors by running:
npx tsx ./index.ts
- The script should run and immediately exit back to your prompt with no error messages. This confirms your TypeScript compiles cleanly.
Seeing a TypeScript error?
- Check that import { z } from "zod" is on its own line. A missing import causes a z is not defined error.
- Make sure the closing }); after the fetchWebpageTool handler is present. Count your braces if needed.
- Help me debug this TypeScript error.
Create the Web Researcher agent with a save_research tool
The fetch tool retrieves raw content, but an agent also needs a way to persist its findings so other agents can read them later. The save_research tool writes the agent's conclusions into the network's shared state.
- In index.ts, add the save_research tool below the fetchWebpageTool definition:
const saveResearchTool = createTool({
name: "save_research",
description: "Save the research findings to the network state",
parameters: z.object({
findings: z.string().describe("The research findings to save"),
}),
handler: async ({ findings }, { network }) => {
if (network) {
network.state.data.web_research = findings;
}
return "Research saved successfully.";
},
});
What does save_research do?
The handler receives a second argument containing the network context. This gives the tool direct access to the shared state object. By writing to network.state.data.web_research, the agent's findings become visible to every other agent in the network.
- Below saveResearchTool, add the Web Researcher agent:
const webResearcher = createAgent({
name: "Web Researcher",
description: "Fetches and analyzes web content to gather research information",
system:
"You are a web research specialist. When given a URL, use the fetch_webpage tool to retrieve its content. " +
"Analyze the content and extract the key information, main points, and relevant details. " +
"Then use the save_research tool to store your findings. Be concise but thorough.",
model,
tools: [fetchWebpageTool, saveResearchTool],
});
Why does the system prompt mention tools by name?
The LLM reads the system prompt to understand its role and capabilities. By explicitly mentioning fetch_webpage and save_research in the prompt, you guide the model to use those tools at the right moment rather than hallucinating an answer.
- Save index.ts and verify the code still compiles by running:
npx tsx ./index.ts
- The script should exit cleanly with no output and no errors, just like before.
Server crashes or shows an error?
- Make sure webResearcher is defined below both tool definitions. JavaScript reads top to bottom, so any reference to fetchWebpageTool or saveResearchTool must come after they are declared.
- Check that tools: [fetchWebpageTool, saveResearchTool] references both tools with exact spelling. A typo in either name causes a reference error.
- Help me find the issue in my index.ts.
Wire the agent into a network and invoke it
Individual agents need a network to orchestrate them. Even with a single agent, the network provides the shared state layer and makes the agent invokable from the Inngest Dev Server.
- In index.ts, add the following at the bottom of the file to create the network and start the server:
const researchNetwork = createNetwork<ResearchState>({
name: "Research Assistant",
agents: [webResearcher],
defaultModel: model,
maxIter: 10,
});
const server = createServer({
agents: [webResearcher],
networks: [researchNetwork],
});
server.listen(3000, () => console.log("AgentKit server running on port 3000!"));
What does createNetwork do?
The createNetwork<ResearchState> call creates a typed network with your ResearchState interface controlling the shared state shape. The maxIter: 10 setting caps how many agent turns can happen before the network stops, preventing infinite loops.
Passing the network to createServer registers it with Inngest so you can invoke it from the Dev Server UI.
- Save index.ts and start the server by running:
npx tsx ./index.ts
- You should see AgentKit server running on port 3000! in the terminal.
Before you invoke the network, what do you think will happen when you ask it to summarize a URL? Will the agent call fetch_webpage automatically, or will it try to answer from memory?
- Switch to your browser and return to the Inngest Dev Server at http://localhost:8288.
- Click the Functions tab in the left sidebar.
- Find the Research Assistant network in the list and click it.
- Click the Invoke button.
- Paste this payload into the JSON editor:
{
"data": {
"input": "Summarize the content at https://inngest.com/ai"
}
}
- Click Invoke Function to send the request.
- Watch the function run appear in the Dev Server. Click into the run to see the step-by-step trace.
You should see the agent call the fetch_webpage tool, retrieve real content from the URL, and return a summary based on the actual page content. The agent did not hallucinate. It fetched live data and reasoned about it.
Function run shows an error?
- If you see a timeout or rate limit error, the free tier has a 20 requests/minute cap. Wait a moment and invoke again.
- If the trace shows the fetch_webpage tool failing, check your internet connection. The tool needs to reach https://inngest.com/ai live.
- If you see no function listed in the Dev Server, restart the Inngest Dev Server in terminal 2:
npx inngest-cli@latest dev -u http://localhost:3000/api/inngest
Help me debug the error in the Inngest trace.
✔️ Awesome, I've got everything!
Great. Double check you have saved index.ts and your server is running without errors.
ⓧ I'd like to double check the full code
Your index.ts should contain the following sections in order. Scroll up and confirm each one is present:
- Imports from @inngest/agent-kit, @inngest/agent-kit/server, and zod.
- The model constant using openai() with the OpenRouter base URL.
- The ResearchState interface with a web_research field.
- The fetchWebpageTool with its fetch, regex strip, and truncation logic.
- The saveResearchTool writing to network.state.data.web_research.
- The webResearcher agent with both tools in its tools array.
- The researchNetwork created with createNetwork<ResearchState>.
- The createServer with agents: [webResearcher] and networks: [researchNetwork].
Your Web Researcher agent can now fetch real web content and persist its findings to shared state. Next up, you will add a second agent with a completely different skill and discover what happens when two agents share a network with no routing logic.
Add the Code Analyzer Agent
Your network currently has one skill: fetching web pages. A real multi-agent system needs specialists with different capabilities.
In this step, you'll give the network a second agent that reads and analyzes local code files. But here's the catch: with two agents and no explicit routing logic, the network won't know which agent to call for a given task. You'll experience this limitation firsthand.
In this step, get ready to:
- Create a file-reading tool that gives an agent access to local code.
- Build a Code Analyzer agent with its own tool for saving analysis results.
- Observe what happens when two agents share a network with no routing logic.
Create the read_file tool
The Code Analyzer agent needs a way to read files from your local machine. You'll create a tool that takes a filename, reads it from a ./files/ directory, and returns the content.
First, the tool needs Node.js file system utilities that aren't imported yet. You also need to expand the ResearchState interface so the network can track code analysis results alongside web research.
- In index.ts, find the existing import line:
import { z } from "zod";
- Add these two imports directly below it:
import { readFileSync } from "fs";
import { join } from "path";
- Now find the ResearchState interface:
interface ResearchState {
web_research?: string;
}
- Add the code_analysis field so it looks like this:
interface ResearchState {
web_research?: string;
code_analysis?: string;
}
- Now add the read_file tool below the saveResearchTool by pasting the following code:
const readFileTool = createTool({
name: "read_file",
description: "Read a file from the local files directory",
parameters: z.object({
filename: z.string().describe("The name of the file to read"),
}),
handler: async ({ filename }) => {
try {
const filePath = join(process.cwd(), "files", filename);
const content = readFileSync(filePath, "utf-8");
return content;
} catch {
return `Error: Could not read file "${filename}". Make sure it exists in the ./files/ directory.`;
}
},
});
What does this code do?
This tool gives an agent the ability to read files from your project's ./files/ directory.
- join(process.cwd(), "files", filename) builds a safe path to the requested file, always scoped to the files/ folder.
- readFileSync reads the file synchronously and returns its content as a string.
- The try/catch block returns a friendly error message if the file doesn't exist, instead of crashing the agent.
- Save index.ts.
- Switch to your first terminal, stop the server with Ctrl+C, and restart it by running:
npx tsx ./index.ts
You should see AgentKit server running on port 3000! confirming there are no syntax errors.
Seeing a compile error?
- Check that import { readFileSync } from "fs"; and import { join } from "path"; are on their own lines below the zod import.
- Confirm you added code_analysis?: string; inside the ResearchState interface block.
- help me fix a TypeScript compile error in my AgentKit project
Create the Code Analyzer agent
The tool can read files, but only an agent can decide when to use it and what to do with the result. You'll create a save_analysis tool for storing findings, then build the Code Analyzer agent itself.
- Add the save_analysis tool below readFileTool by pasting this code:
const saveAnalysisTool = createTool({
name: "save_analysis",
description: "Save the code analysis results to the network state",
parameters: z.object({
analysis: z.string().describe("The code analysis results to save"),
}),
handler: async ({ analysis }, { network }) => {
if (network) {
network.state.data.code_analysis = analysis;
}
return "Analysis saved successfully.";
},
});
What does this code do?
This tool writes the Code Analyzer's findings to network.state.data.code_analysis. It follows the same pattern as saveResearchTool from the previous step. Storing results in shared state is how agents in a network pass information to each other.
- Save index.ts, restart the server, and confirm you see the startup message again.
Now create the agent that uses both tools.
- Add the Code Analyzer agent below the webResearcher agent by pasting this code:
const codeAnalyzer = createAgent({
name: "Code Analyzer",
description: "Reads and analyzes local code files for structure, patterns, and issues",
system:
"You are a code analysis expert. Use the read_file tool to read code files from the local directory. " +
"Analyze the code structure, identify patterns, dependencies, and potential issues. " +
"Then use the save_analysis tool to store your findings. Focus on architecture and design patterns.",
model,
tools: [readFileTool, saveAnalysisTool],
});
What does this code do?
This agent specializes in reading and analyzing code. Its system prompt instructs it to use read_file to access source files, then save_analysis to record its findings.
The tools array restricts which tools this agent can call. The Web Researcher cannot read files, and the Code Analyzer cannot fetch web pages. Each agent has a focused skill set.
- Now register the new agent in the network. Find the researchNetwork definition:
const researchNetwork = createNetwork<ResearchState>({
name: "Research Assistant",
agents: [webResearcher],
defaultModel: model,
maxIter: 10,
});
- Update the agents array to include codeAnalyzer:
const researchNetwork = createNetwork<ResearchState>({
name: "Research Assistant",
agents: [webResearcher, codeAnalyzer],
defaultModel: model,
maxIter: 10,
});
- Also find the createServer call and update it to include codeAnalyzer:
const server = createServer({
agents: [webResearcher, codeAnalyzer],
networks: [researchNetwork],
});
- Save index.ts, restart the server, and confirm it prints AgentKit server running on port 3000! without errors.
- Switch to your second terminal and check the Inngest Dev Server at http://localhost:8288/functions. You should see the Research Assistant network listed with both agents available.
Only seeing one agent?
- Make sure you added codeAnalyzer to BOTH the agents array in createNetwork and the agents array in createServer.
- Confirm you restarted the AgentKit server after saving.
- help me register multiple agents in my AgentKit network
Test the network without a router
Both agents are registered, but the network has no routing logic. When you send a request, the network's default router (an LLM-based decision maker) will try to pick which agent to call. Let's see what happens with a code analysis request.
First, the Code Analyzer needs a file to read.
- Create the files directory and example.ts file inside it by running these commands from your project root:
mkdir files
- Create files/example.ts and paste the following sample code into it:
import express from "express";
import { z } from "zod";
const app = express();
app.use(express.json());
// Schema validation for user input
const UserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
role: z.enum(["admin", "user", "viewer"]),
});
type User = z.infer<typeof UserSchema>;
// In-memory store (would be a database in production)
const users: Map<string, User> = new Map();
app.post("/users", (req, res) => {
const result = UserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.issues });
}
const id = crypto.randomUUID();
users.set(id, result.data);
return res.status(201).json({ id, ...result.data });
});
app.get("/users/:id", (req, res) => {
const user = users.get(req.params.id);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
return res.json(user);
});
app.listen(4000, () => console.log("API running on port 4000"));
What is this sample file?
This is a small Express API with Zod validation that the Code Analyzer agent will read and analyze. It's not code your project runs. It's a sample for the agent to practice on.
- Confirm the file exists by running:
ls files
You should see example.ts in the output.
Now invoke the network from the Dev Server. Before you do, think about what you expect to happen. You have two agents (Web Researcher and Code Analyzer) but no explicit routing logic. Will the network know to send a code analysis request to the Code Analyzer?
- Open the Inngest Dev Server at http://localhost:8288.
- Navigate to the Research Assistant network function.
- Click Invoke and enter this payload:
{"data": {"input": "Analyze the code in example.ts and identify its patterns and structure"}}
- Click Invoke Function and watch the execution trace.
Watch the Dev Server trace closely. Without a deterministic router, the network's default routing agent uses an LLM call to decide which agent to run. You'll likely see one of these outcomes:
- The Web Researcher runs instead of the Code Analyzer (wrong agent for the task).
- The agents loop between each other without converging on a useful answer.
- The network hits maxIter and stops without a clean result.
Why does this happen?
Without a custom router, AgentKit uses a default LLM-based routing agent to pick which agent runs next. This works for simple cases, but it has no knowledge of your network's state or which tasks have already been completed.
The result is unpredictable. Sometimes the right agent runs. Sometimes the wrong one does. There's no guarantee of correct sequencing, and no way to enforce that each agent contributes before the network stops. This is exactly the problem a deterministic state-based router solves.
✔️ Awesome, I've got everything!
Great. Double check you've saved both index.ts and files/example.ts.
ⓧ I'd like to double check the full code
Scroll up and confirm your index.ts contains each of these sections in order:
- Imports: @inngest/agent-kit, @inngest/agent-kit/server, zod, fs, path.
- The model constant configured with OpenRouter.
- ResearchState interface with both web_research and code_analysis fields.
- Four tools: fetchWebpageTool, saveResearchTool, readFileTool, saveAnalysisTool.
- Two agents: webResearcher and codeAnalyzer.
- researchNetwork with both agents in the agents array (no router).
- createServer listing both agents and the network.
- server.listen(3000, ...) at the bottom.
For files/example.ts, confirm it contains the full Express + Zod user API sample code with the POST /users and GET /users/:id routes.
You've proven the problem: two agents without routing logic can't reliably coordinate. In the next step, you'll build a deterministic state-based router that inspects shared state and decides exactly which agent acts next.
Implement Deterministic State-Based Routing
The previous step exposed a critical gap. Two agents with distinct skills sat in the same network, but without direction, the system had no way to decide who should act or when to stop.
The solution is a router. A router function inspects the network's shared state after each agent runs and picks which agent goes next. When every task is complete, it returns undefined to stop execution. This is deterministic state-based routing. The agents themselves stay focused on their specialty. The router coordinates.
In this step, get ready to:
- Add a Summarizer agent that synthesizes outputs from the other agents.
- Implement a deterministic router that sequences agents based on shared state.
- Test the full three-agent pipeline and observe the execution trace in the Dev Server.
Update the network state and add a Summarizer agent
The Summarizer needs a place to store its output in shared state. You also need a tool it can call and a dynamic system prompt that reads the other agents' findings.
- In index.ts, find the ResearchState interface near the top of the file.
- Find this code:
interface ResearchState {
web_research?: string;
code_analysis?: string;
}
- Add a summary field so the interface now looks like this:
interface ResearchState {
web_research?: string;
code_analysis?: string;
summary?: string;
}
- Next, add the saveSummaryTool below your existing saveAnalysisTool. Copy this code:
const saveSummaryTool = createTool({
name: "save_summary",
description: "Save the final summary to the network state",
parameters: z.object({
summary: z.string().describe("The final synthesized summary"),
}),
handler: async ({ summary }, { network }) => {
if (network) {
network.state.data.summary = summary;
}
return "Summary saved successfully.";
},
});
What does this tool do?
Like the other save tools, saveSummaryTool writes the Summarizer's output into the shared network state. The router will check this field to know when the pipeline is complete.
- Now add the Summarizer agent below the codeAnalyzer agent. Copy this code:
const summarizer = createAgent({
name: "Summarizer",
description:
"Synthesizes research and analysis into a cohesive final report",
system: ({ network }) => {
const research = network?.state.data.web_research || "No web research available.";
const analysis = network?.state.data.code_analysis || "No code analysis available.";
return (
"You are a synthesis specialist. Your job is to combine research findings and code analysis into a clear, " +
"actionable summary. Here is what the other agents found:\n\n" +
`WEB RESEARCH:\n${research}\n\n` +
`CODE ANALYSIS:\n${analysis}\n\n` +
"Synthesize these into a cohesive brief. Use the save_summary tool to store your final output."
);
},
model,
tools: [saveSummaryTool],
});
What makes this agent different?
Unlike the other agents, the Summarizer uses a dynamic system prompt. Instead of a static string, system is a function that receives the current network state. Every time the Summarizer is called, it reads the latest research and analysis directly from shared state.
This means the Summarizer always sees the most up-to-date findings from the other agents, no matter when in the pipeline it runs.
- Save index.ts.
- Stop the AgentKit server in terminal 1 with Ctrl+C, then restart it by running:
npx tsx ./index.ts
You should see AgentKit server running on port 3000! with no TypeScript errors. This confirms the new tool and agent compile correctly.
Seeing a TypeScript error?
- Check that saveSummaryTool is defined above the summarizer agent. TypeScript reads top-to-bottom, so tools must be declared before agents that reference them.
- Confirm the summary field is spelled exactly as summary?: string in your ResearchState interface.
Help me debug this TypeScript error in my AgentKit project.
Implement the deterministic router
The router is a function that runs after each agent completes. It looks at the network's shared state and returns the next agent to call, or undefined to stop execution.
- In index.ts, find the existing researchNetwork block. It currently looks like this:
const researchNetwork = createNetwork<ResearchState>({
name: "Research Assistant",
agents: [webResearcher, codeAnalyzer],
defaultModel: model,
maxIter: 10,
});
- Replace the entire researchNetwork block with this version that includes the router and all three agents:
const researchNetwork = createNetwork<ResearchState>({
name: "Research Assistant",
agents: [webResearcher, codeAnalyzer, summarizer],
defaultModel: model,
maxIter: 10,
router: ({ network }) => {
if (!network.state.data.web_research) {
return webResearcher;
}
if (!network.state.data.code_analysis) {
return codeAnalyzer;
}
if (!network.state.data.summary) {
return summarizer;
}
return undefined;
},
});
How does this router work?
The router checks shared state top-to-bottom. If web_research is empty, it routes to the Web Researcher. If code_analysis is empty, it routes to the Code Analyzer. If both exist but summary is missing, it routes to the Summarizer.
When all three fields are populated, the router returns undefined and the network stops. This is deterministic because the execution order is defined entirely by state, not by LLM decisions.
- Now find the createServer block just below. It currently looks like this:
const server = createServer({
agents: [webResearcher, codeAnalyzer],
networks: [researchNetwork],
});
- Add summarizer to the agents array so it looks like this:
const server = createServer({
agents: [webResearcher, codeAnalyzer, summarizer],
networks: [researchNetwork],
});
- Save index.ts.
- Stop the server with Ctrl+C and restart it by running:
npx tsx ./index.ts
You should see AgentKit server running on port 3000! confirming the router and all three agents are wired up.
Server not starting?
- Verify summarizer is declared above the researchNetwork block. The router references it, so it must exist first.
- Make sure the agents array in both createNetwork and createServer includes all three: webResearcher, codeAnalyzer, summarizer.
Help me fix my router setup.
Test the full three-agent pipeline
Everything is wired. Time to invoke the network and watch three agents collaborate in sequence.
- Switch to the Inngest Dev Server in your browser at http://localhost:8288.
- Click on the Research Assistant network in the functions list.
- Click Invoke to open the invocation form.
Before you run this, what do you think the execution order will be? Which agent will the router pick first, second, and third?
- Paste this payload into the invoke form and send it:
{
"data": {
"input": "Research what Inngest AgentKit does from https://agentkit.inngest.com then analyze the code in example.ts and provide a combined summary"
}
}
Watch the Dev Server trace populate in real time. You should see the following sequence:
- Router selects Web Researcher (web_research is empty).
- Web Researcher calls fetch_webpage, then save_research.
- Router selects Code Analyzer (code_analysis is empty).
- Code Analyzer calls read_file, then save_analysis.
- Router selects Summarizer (summary is empty).
- Summarizer calls save_summary.
- Router returns undefined (all fields populated). Network stops.
Why is this powerful?
No agent knows about the others. The Web Researcher just fetches and saves. The Code Analyzer just reads and analyzes. The Summarizer just synthesizes. The router is the only piece that understands the full workflow.
This separation means you can add, remove, or reorder agents by changing only the router logic. The agents themselves never need to change.
Network not completing all three stages?
- If the network loops or stops early, check that each agent's save tool writes to the exact field the router checks: web_research, code_analysis, summary.
- If the Web Researcher fails to fetch the URL, it might not call save_research. The router will keep selecting it. Check the trace for fetch errors.
- Make sure maxIter: 10 is set. Without it, the network might stop too early.
My multi-agent network isn't completing all three stages.
✔️ Awesome, I've got everything!
Great. Double check you've saved index.ts and your server is running without errors.
ⓧ I'd like to double check the full code
Your index.ts should now contain these sections in order. Scroll through your file and confirm each one is present:
- Imports (createAgent, createNetwork, createTool, openai, createServer, z, readFileSync, join).
- Model configuration with openai() pointing to OpenRouter.
- ResearchState interface with three fields: web_research, code_analysis, summary.
- Five tools: fetchWebpageTool, saveResearchTool, readFileTool, saveAnalysisTool, saveSummaryTool.
- Three agents: webResearcher, codeAnalyzer, summarizer (with a dynamic system function).
- researchNetwork with all three agents, maxIter: 10, and a router function checking state fields in order.
- createServer listing all three agents and the network.
- server.listen(3000, ...) at the bottom.
Your multi-agent network now has a brain. The router orchestrates three specialists in sequence, and you can watch the entire flow in the Dev Server. Next up, you'll see what happens when things go wrong and discover why durable execution matters for AI pipelines.
Observe Durable Execution and Retry Behavior
Your multi-agent network works perfectly when everything goes right. But real-world tools fail. URLs timeout, APIs rate-limit, files get moved.
This step deliberately breaks the web fetch tool to show Inngest's killer feature: when a step fails, only that step retries while all completed work is preserved. You'll see why durable execution matters for AI pipelines.
In this step, get ready to:
- Break the fetch tool to trigger a failure and observe retry behavior.
- Watch the Dev Server trace show retries with exponential backoff while preserving completed steps.
- Fix the tool and watch the network recover from where it left off.
Break the fetch tool and trigger a failure
You'll hardcode a URL that always returns a server error, so the fetch_webpage tool throws every time. This simulates a real-world scenario where an external API goes down mid-pipeline.
- In your first terminal, stop the AgentKit server by pressing Ctrl+C.
- In index.ts, find the fetchWebpageTool handler. Locate this line inside it:
const response = await fetch(url);
- Replace it with a hardcoded URL that always returns a 500 error, so the line now looks like this:
const response = await fetch("https://httpstat.us/500");
What does this change do?
Instead of fetching the URL the LLM passes in, the tool now always hits https://httpstat.us/500. This is a free testing service that returns whatever HTTP status code you put in the URL.
Since a 500 response means response.ok is false, the handler throws an error on every attempt. That thrown error is what triggers Inngest's retry mechanism.
- Save index.ts.
- Restart the AgentKit server by running:
npx tsx ./index.ts
Before you invoke the network, think about this: what do you expect will happen when the Web Researcher's tool fails? Will the entire function restart from scratch, or will something smarter happen?
- Switch to the Inngest Dev Server at http://localhost:8288/functions.
- Click Trigger on the Research Assistant network.
- In the modal, enter this payload and click Run:
{
"input": "Summarize the content at https://inngest.com/ai then analyze the code in example.ts and provide a combined summary"
}
- Switch to the Runs tab and click into the new run.
You should see the function run start, the Web Researcher agent invoke, and then the fetch step fail with an error. The run enters a retry state instead of completing.
Not seeing a failed step?
Make sure you saved index.ts and restarted the server after making the change. The old running process still has the working code in memory.
Also confirm the Inngest Dev Server (terminal 2) is still running and connected to your app.
help me debug why my fetch tool isn't failing as expected
Observe retry behavior and durable execution
Now look closely at what the Dev Server is showing you. This is where durable execution becomes visible.
- In the run detail view, observe the step timeline. The failed step shows a retry counter incrementing.
- Notice the timing between retries. Each retry waits longer than the last. This is exponential backoff.
- Look at the overall function run status. It shows as retrying or paused. It has not restarted from the beginning.
Why does only the failed step retry?
Each tool call in an AgentKit network becomes a durable step inside the Inngest function. When step N fails, only step N retries. Completed steps return their cached results instantly.
In a naive implementation, the entire function would restart from scratch. That means re-running the LLM call that decided to use the tool (wasting tokens and money). Inngest's approach preserves every completed step and only retries the one that failed.
Fix the tool and watch the network recover
The function is paused, waiting for the next retry window. If you fix the tool and restart the server before that retry fires, the step will succeed on the next attempt and the network will continue from where it left off.
- In your first terminal, stop the AgentKit server by pressing Ctrl+C.
- In index.ts, find the hardcoded URL you just added:
const response = await fetch("https://httpstat.us/500");
- Change it back to the original dynamic URL, so the line reads:
const response = await fetch(url);
- Save index.ts.
- Restart the AgentKit server by running:
npx tsx ./index.ts
Now predict: when the next retry fires, will the Web Researcher re-run the LLM call to decide which tool to use, or will it skip straight to re-executing the tool?
- Switch back to the Inngest Dev Server and watch the run detail for the same function run.
- Wait for the next retry to fire (typically a few seconds in the Dev Server).
You should see the fetch step succeed on this retry. The network then continues to the Code Analyzer and Summarizer without re-running the Web Researcher's LLM decision step. The full trace now shows: initial attempt (failed), retries, success, then continuation through all three agents.
What makes this different from a restart?
If this were a traditional script, the crash would kill the process. You'd rerun it from the top, re-calling the LLM, re-making the same decision, and then retrying the fetch. That costs time and money.
Inngest's durable steps cached the LLM's decision. On retry, only the fetch_webpage call re-executes. Once it succeeds, the network picks up exactly where it left off and routes to the next agent.
Run completed but shows errors?
If the run still fails after you fix the code, the retry window may have already exhausted its attempts. Trigger a fresh run from the Functions tab with the same payload to see the full successful pipeline with the fixed code.
help me understand why my Inngest function still shows failed after I fixed the code
✔️ Awesome, I've got everything!
Great. Double check that index.ts is reverted to use await fetch(url) and your server is running.
ⓧ I'd like to double check the full code
Your index.ts should be back to its original working state. Confirm the fetchWebpageTool handler has this line using the dynamic url parameter:
handler: async ({ url }) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status}`);
}
const html = await response.text();
const text = html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
return text.slice(0, 3000);
},
Everything else in the file (agents, router, network, server) should be unchanged from the end of Step 4.
You've just seen Inngest's durable execution in action. Failed steps retry independently while completed work is preserved. This is what makes multi-agent systems production-ready.
Secret mission
Add a Fact Checker Agent
Your network currently trusts whatever the Web Researcher and Code Analyzer produce. What if their findings contain unverified claims or logical inconsistencies? Add a Fact Checker agent that validates outputs before the Summarizer runs, extending your routing logic and proving how easy it is to insert new agents into an existing network.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with a free OpenRouter tier, so there are no ongoing costs.
Resources you used:
- Your project folder on the Desktop (contains index.ts, tsconfig.json, node_modules/, and the files/ directory).
- The AgentKit server process running on port 3000.
- The Inngest Dev Server process running on port 8288.
Keep everything running
No action needed. Choose this if you're still actively building or experimenting with your multi-agent network.
- Your project files stay on your Desktop.
- The AgentKit server and Inngest Dev Server remain running while your terminal sessions are open.
- Your OpenRouter account remains free with no charges. The OPENAI_API_KEY environment variable is session-scoped and disappears when you close the terminal.
Pause - I'll come back to this later
Shut down running processes to free up your terminal sessions and ports. Your project files stay on disk for when you return.
- Press Ctrl+C in the terminal running the AgentKit server to stop it.
- Press Ctrl+C in the terminal running the Inngest Dev Server to stop it.
- To resume later, re-export your OPENAI_API_KEY environment variable in a new terminal, then start both servers again.
Delete - I don't want to use this again
Remove all project files and stop running processes. This fully cleans up everything from your machine.
- Press Ctrl+C in the terminal running the AgentKit server to stop it.
- Press Ctrl+C in the terminal running the Inngest Dev Server to stop it.
- Delete your project folder from the Desktop by running this command (macOS/Linux):
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]
On Windows, run this instead:
cd ~\Desktop
Remove-Item -Recurse -Force [[PROJECT_FOLDER="your project folder name"]]
- This removes all project files including index.ts, tsconfig.json, the files/ directory, and node_modules/.
Nice Work!
Nice Work!
You just built a fully orchestrated multi-agent research system using Inngest AgentKit. Your agents collaborate through shared state, call real tools, and recover from failures automatically.
You learned how to:
- Built a multi-agent network with three specialized agents (Web Researcher, Code Analyzer, Summarizer) that collaborate through shared network state and deterministic routing.
- Created custom tools that give agents real-world capabilities. Your agents fetch live web content and read local files, with the LLM deciding when to call each tool.
- Observed durable execution in the Inngest Dev Server. You watched a failed step retry independently while all completed work stayed preserved.
- Secret Mission: Added a Fact Checker agent that validates claims from the other agents before the Summarizer produces its final output.
Ready to quiz yourself?