Introduction
Welcome to Chapter 7! In the rapidly evolving world of software, AI agents are becoming indispensable for automating complex, multi-step tasks that require reasoning, planning, and interaction with external tools. Imagine a system that can understand a user’s request, break it down into smaller problems, use various tools (like APIs or databases) to gather information, and then formulate a coherent response or take action—all without constant human supervision. That’s the power of AI agents.
This chapter will guide you through building such intelligent, automated systems using Trigger.dev. We’ll explore how Trigger.dev’s robust features like durable execution, retries, and observability provide the perfect foundation for creating reliable and scalable AI agents. By the end, you’ll understand the core concepts of agentic workflows and have built your first Trigger.dev-powered agent.
You should be familiar with the basics of Trigger.dev jobs, durable execution, and project setup from previous chapters. We’ll build upon that knowledge to integrate Large Language Models (LLMs) and external tools into sophisticated workflows.
Core Concepts of AI Agents in Trigger.dev
Before we dive into code, let’s establish a clear understanding of what AI agents are and why Trigger.dev is an excellent choice for orchestrating them.
What is an AI Agent?
At its heart, an AI agent is a system designed to perceive its environment, make decisions, and take actions to achieve specific goals. Unlike simple scripts that follow a predefined path, agents can adapt to new information, recover from errors, and often learn from their experiences.
A common pattern for AI agents, especially those powered by LLMs, involves a loop:
- Observe: Understand the current state, user input, or system events.
- Plan: Based on the observation and goal, decide the next step. This might involve choosing a tool, generating a sub-task, or refining the objective.
- Act: Execute the planned step, often by calling an external tool (e.g., an API, database query, or another Trigger.dev job).
- Reflect: Evaluate the outcome of the action. Did it succeed? Is the goal closer? What went wrong? Use this to refine the plan or adjust future actions.
This iterative process allows agents to tackle complex problems by breaking them down into manageable steps.
Anatomy of a basic AI Agent’s decision loop.
Why Trigger.dev for Agents?
Building robust AI agents presents unique challenges:
- Long-running tasks: Agent workflows can involve multiple steps, API calls, and human interactions, potentially spanning minutes, hours, or even days.
- External dependencies: Agents rely heavily on external tools (LLM providers, third-party APIs) which can be unreliable.
- State management: Maintaining context and progress across multiple steps and retries is crucial.
- Observability: Understanding an agent’s decision-making process and execution path is vital for debugging and auditing.
Trigger.dev directly addresses these challenges:
- Durable Execution: Trigger.dev jobs are inherently durable. If your server crashes or an external API takes too long, Trigger.dev automatically saves the agent’s state and resumes it exactly where it left off. This is critical for long-running agentic workflows.
- Built-in Retries: When an agent attempts to use a tool (e.g., makes an API call) and it fails due to a transient error, Trigger.dev’s declarative retry mechanisms automatically re-attempt the operation, improving agent resilience.
- Queues and Concurrency: Agents can generate many sub-tasks or calls to external tools. Trigger.dev’s queuing system handles these efficiently, allowing you to scale your agent’s workload without overwhelming your backend or external services.
- Observability: Every step of a Trigger.dev job, including tool invocations and state changes within your agent, is logged and visible in the Trigger.dev dashboard. This provides an invaluable “trace” of your agent’s reasoning and actions.
⚡ Real-world insight: Imagine an AI agent tasked with processing customer support tickets. It might call a sentiment analysis API, then a CRM API, then draft an email, and finally wait for human approval. Each of these steps could fail or take time. Trigger.dev ensures that this complex, multi-stage process doesn’t fall apart, even if the underlying infrastructure is flaky.
Agentic Workflow Design Principles
When designing agents with Trigger.dev, consider these principles:
- Tool-Use Focus: Agents gain their power by interacting with tools. Design your tools as distinct, reusable functions that the LLM can invoke.
- Explicit State Management: While Trigger.dev handles job state, your agent’s internal reasoning state (e.g., “current plan,” “remaining tasks”) should be managed explicitly within your job’s variables.
- Clear Objectives: Define what success looks like for your agent. This helps the LLM focus and allows for better reflection.
- Human-in-the-Loop: For critical or uncertain tasks, design points where a human can review, approve, or override an agent’s decision. Trigger.dev’s ability to pause and resume jobs is perfect for this.
MCP Integration: A Note
The term “MCP integration” is not widely documented within the public Trigger.dev ecosystem as of 2026-05-20. It’s possible this refers to an internal Trigger.dev component, a specific partner integration not yet publicly detailed, or a general pattern for “Multi-Cloud Platform” or “Management Control Plane” integration.
For the purpose of this guide, we will focus on the well-established patterns of integrating AI agents with external APIs and services, which Trigger.dev excels at. If “MCP” refers to a specific Trigger.dev feature that emerges with v4 GA, it would likely involve enhanced control plane functionalities for managing complex, distributed agent deployments. Until then, treat your external LLM providers and custom tools as the primary integrations for your agents.
Building Your First Trigger.dev AI Agent
Let’s get practical! We’ll create a simple AI agent that takes a topic, uses an LLM to generate a short summary, and then (as a tool) “publishes” that summary.
Setting Up the Project
First, ensure you have your Trigger.dev v4-beta project set up. If you haven’t yet, create a new project:
npx trigger.dev@v4-beta init
Follow the prompts to set up your project. This will create a basic Trigger.dev project with a trigger.ts file and an example job.
Next, we’ll need an LLM client. For this example, we’ll use OpenAI’s API. Install the required package:
npm install openai@4.x.x
# or yarn add openai@4.x.x
(Note: As of 2026-05-20, OpenAI’s official openai package is at version 4.x.x. Always check their official documentation for the latest recommended version.)
You’ll also need an OPENAI_API_KEY. Add this to your .env file:
# .env
OPENAI_API_KEY="sk-your-openai-api-key"
Defining a Simple Agentic Job
We’ll start by creating a job that orchestrates a simple LLM call. Open src/trigger.ts (or src/jobs/myAgentJob.ts if you prefer modular files) and add the following:
// src/trigger.ts
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
import OpenAI from "openai";
// Ensure you have your Trigger.dev project ID and API key configured
export const client = new TriggerClient({
id: "your-project-id", // Replace with your Trigger.dev Project ID
apiKey: process.env.TRIGGER_API_KEY,
});
// Initialize OpenAI client
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
client.defineJob({
id: "ai-summarizer-agent",
name: "AI Summarizer Agent",
version: "1.0.0",
trigger: eventTrigger({
name: "summarize.topic",
schema: {
type: "object",
properties: {
topic: { type: "string" },
length: { type: "string", default: "short" },
},
required: ["topic"],
additionalProperties: false,
}
}),
run: async (payload, io, ctx) => {
await io.logger.info(`Starting summarization for topic: ${payload.topic}`);
// Step 1: Call the LLM to generate a summary
const completion = await io.runTask(
"generate-summary-with-llm",
async () => {
const response = await openai.chat.completions.create({
model: "gpt-4o", // Using the latest model as of 2026-05-20
messages: [
{
role: "system",
content: `You are an expert summarization agent. Summarize the provided topic concisely. The summary should be ${payload.length}.`,
},
{ role: "user", content: payload.topic },
],
temperature: 0.7,
max_tokens: 150,
});
return response.choices[0].message.content;
},
{
name: "Generate Summary with OpenAI",
properties: {
topic: payload.topic,
model: "gpt-4o",
},
}
);
if (!completion) {
await io.logger.error("LLM did not return a summary.");
throw new Error("Failed to generate summary.");
}
await io.logger.info(`Generated summary: ${completion}`);
// Step 2: "Publish" the summary (simulate a tool call)
await io.runTask(
"publish-summary",
async () => {
// In a real agent, this would be an API call to a publishing service,
// a database write, or a notification.
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate async work
await io.logger.info(`Summary "${completion.substring(0, 30)}..." published.`);
return { status: "success", summary: completion };
},
{
name: "Publish Summary Tool",
properties: {
summaryPreview: completion.substring(0, 50),
},
}
);
return {
status: "success",
originalTopic: payload.topic,
generatedSummary: completion,
};
},
});
Here’s a breakdown of what we added:
openaiimport and initialization: We bring in the OpenAI client and initialize it using yourOPENAI_API_KEY.client.defineJob: We define a new job namedai-summarizer-agent.eventTrigger: This job is triggered by a custom eventsummarize.topicwhich expects atopic(string) and optionally alength(string).io.logger.info: We use Trigger.dev’sio.loggerto log the agent’s progress, which will appear in your Trigger.dev dashboard.io.runTask("generate-summary-with-llm", ...): This is crucial.io.runTaskwraps asynchronous operations, making them durable and retryable.- Inside, we call
openai.chat.completions.createusing thegpt-4omodel (latest as of 2026-05-20) to generate a summary based on thepayload.topicandpayload.length. - The
nameandpropertiesarguments toio.runTaskprovide excellent observability, allowing you to see exactly what the LLM call was about in the dashboard.
- Inside, we call
- Error Handling: We check if
completionis returned and throw an error if not, demonstrating how Trigger.dev jobs can handle failures. io.runTask("publish-summary", ...): This simulates an external tool call. In a real application, this would be an actual API call to save the summary, send it to another service, or update a database.- Return Value: The job returns a structured object indicating success and providing the original topic and generated summary.
To run this locally, ensure your Trigger.dev development server is active:
npm run dev
Then, trigger the event from your Trigger.dev dashboard or using the io.sendEvent method in another job. For example, you can send an event via the dashboard’s “Send an event” feature with:
{
"name": "summarize.topic",
"payload": {
"topic": "The history of quantum computing",
"length": "medium"
}
}
Observe how Trigger.dev logs each step, including the LLM call and the simulated publish action, providing a clear trace of your agent’s execution.
Adding Tools: Enhancing Agent Capabilities
Our current agent is simple. True AI agents leverage tools—functions that allow them to interact with the external world beyond just generating text. Let’s imagine our agent needs to fetch information before summarizing.
We’ll add a simple “search” tool. For demonstration, this tool will just return a predefined string, but in a real scenario, it would call a search API (e.g., Google Search, internal knowledge base).
First, let’s define our tools. We’ll add a new utility file for this, say src/tools.ts:
// src/tools.ts
import { io } from "@trigger.dev/sdk";
export async function searchTool(query: string): Promise<string> {
return io.runTask(
`search-tool-${query.substring(0, 20).replace(/\s/g, "-")}`, // Dynamic ID for observability
async () => {
await io.logger.info(`Simulating search for: "${query}"`);
// In a real scenario, this would be an API call to a search engine
// e.g., const response = await axios.get(`https://api.search.com?q=${query}`);
// return response.data;
await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate network delay
if (query.toLowerCase().includes("quantum computing")) {
return "Quantum computing uses quantum-mechanical phenomena like superposition and entanglement to perform computations. It can solve problems intractable for classical computers.";
} else if (query.toLowerCase().includes("machine learning")) {
return "Machine learning is a subset of AI that enables systems to learn from data, identify patterns, and make decisions with minimal human intervention.";
}
return `No specific information found for "${query}".`;
},
{
name: `Search Tool: ${query}`,
properties: {
query: query,
},
}
);
}
export async function publishSummaryTool(summary: string): Promise<{status: string, summary: string}> {
return io.runTask(
"publish-summary-tool",
async () => {
await io.logger.info(`Attempting to publish summary: "${summary.substring(0, 50)}..."`);
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate publishing delay
return { status: "success", summary: summary };
},
{
name: "Publish Summary Tool",
properties: {
summaryPreview: summary.substring(0, 50),
},
}
);
}
Now, let’s modify our ai-summarizer-agent job in src/trigger.ts to use these tools by having the LLM decide when to call them. This involves using OpenAI’s “function calling” capability.
// src/trigger.ts
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
import OpenAI from "openai";
import { searchTool, publishSummaryTool } from "./tools"; // Import our new tools
export const client = new TriggerClient({
id: "your-project-id", // Replace with your Trigger.dev Project ID
apiKey: process.env.TRIGGER_API_KEY,
});
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
client.defineJob({
id: "ai-smart-agent-with-tools", // New job ID
name: "AI Smart Agent with Tools",
version: "1.0.0",
trigger: eventTrigger({
name: "agent.request",
schema: {
type: "object",
properties: {
user_prompt: { type: "string" },
},
required: ["user_prompt"],
additionalProperties: false,
}
}),
run: async (payload, io, ctx) => {
await io.logger.info(`Agent received request: ${payload.user_prompt}`);
let messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{
role: "system",
content: `You are a helpful AI assistant. You have access to tools to help you answer questions or perform tasks.
You should first consider if you need to search for information before answering.
If you generate a summary, you should use the 'publishSummaryTool' to make it available.`,
},
{ role: "user", content: payload.user_prompt },
];
const availableTools = {
searchTool: searchTool,
publishSummaryTool: publishSummaryTool,
};
const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
{
type: "function",
function: {
name: "searchTool",
description: "Searches for information on a given query.",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "The search query.",
},
},
required: ["query"],
},
},
},
{
type: "function",
function: {
name: "publishSummaryTool",
description: "Publishes a generated summary.",
parameters: {
type: "object",
properties: {
summary: {
type: "string",
description: "The summary text to publish.",
},
},
required: ["summary"],
},
},
},
];
let loopCount = 0;
const MAX_LOOP_COUNT = 5; // Prevent infinite loops
while (loopCount < MAX_LOOP_COUNT) {
const chatCompletion = await io.runTask(
`agent-llm-call-${loopCount}`,
async () => {
return openai.chat.completions.create({
model: "gpt-4o",
messages: messages,
tools: tools,
tool_choice: "auto", // Allow the model to choose to call a tool or respond
temperature: 0.7,
});
},
{
name: `LLM Call ${loopCount}`,
properties: {
messages: messages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content.substring(0, 100) : 'complex content' })),
tools: tools.map(t => t.function.name),
},
}
);
const responseMessage = chatCompletion.choices[0].message;
messages.push(responseMessage); // Add LLM's response to conversation history
if (responseMessage.tool_calls) {
// Step: Act - LLM wants to call a tool
await io.logger.info(`LLM requested tool calls: ${JSON.stringify(responseMessage.tool_calls)}`);
for (const toolCall of responseMessage.tool_calls) {
const functionName = toolCall.function.name;
const functionArgs = JSON.parse(toolCall.function.arguments);
if (functionName in availableTools) {
const toolToCall = availableTools[functionName as keyof typeof availableTools];
const toolOutput = await toolToCall(functionArgs.query || functionArgs.summary); // Pass args dynamically
messages.push({
tool_call_id: toolCall.id,
role: "tool",
name: functionName,
content: JSON.stringify(toolOutput),
});
await io.logger.info(`Tool '${functionName}' executed. Output: ${JSON.stringify(toolOutput).substring(0, 100)}`);
} else {
const errorMessage = `Tool '${functionName}' not found.`;
messages.push({
tool_call_id: toolCall.id,
role: "tool",
name: functionName,
content: errorMessage,
});
await io.logger.error(errorMessage);
}
}
} else {
// Step: Respond - LLM has a final answer or message
await io.logger.info(`LLM final response: ${responseMessage.content}`);
return {
status: "completed",
finalResponse: responseMessage.content,
conversationHistory: messages,
};
}
loopCount++;
}
// If we reach here, the agent hit the loop limit
await io.logger.warn("Agent reached maximum loop count without a final response.");
return {
status: "failed",
finalResponse: "Agent failed to complete task within loop limit.",
conversationHistory: messages,
};
},
});
Let’s break down the changes:
ai-smart-agent-with-toolsjob: This new job takes auser_prompt.messagesarray: This array holds the entire conversation history, including system instructions, user prompts, LLM responses, and tool outputs. This is how the LLM maintains context.availableToolsandtoolsarrays:availableToolsis a JavaScript object mapping tool names to their actual function implementations.toolsis an array of objects describing the tools to the OpenAI API, including theirname,description, andparametersschema. This allows the LLM to understand when and how to call a tool.
- Agent Loop (
while (loopCount < MAX_LOOP_COUNT)): This is the core of our agent.openai.chat.completions.createwithtools: The LLM is now invoked with thetoolsdefinition. It can decide to either generate a text response or request to call one of the provided tools.tool_choice: "auto"allows this flexibility.responseMessage.tool_callscheck: If the LLM decides to call a tool,responseMessage.tool_callswill be present.- Tool Execution: We iterate through the requested tool calls, parse their arguments, and then execute the actual JavaScript function from our
availableToolsobject. - Adding Tool Output to
messages: Crucially, the output of the tool call is then added back to themessagesarray withrole: "tool". This feeds the tool’s result back to the LLM for its next turn of reasoning. - Final Response: If the LLM doesn’t request a tool call, it means it has a final text response, and the job completes.
MAX_LOOP_COUNT: A safety mechanism to prevent runaway agents.
To test this, send an event to agent.request (via Trigger.dev dashboard or io.sendEvent):
{
"name": "agent.request",
"payload": {
"user_prompt": "Tell me about machine learning and then publish a short summary of it."
}
}
Or try:
{
"name": "agent.request",
"payload": {
"user_prompt": "Explain quantum computing and publish a summary."
}
}
Observe how Trigger.dev logs each LLM call and each tool invocation. You’ll see the agent first calls searchTool, then uses that information to generate a summary, and finally calls publishSummaryTool. This demonstrates the Observe -> Plan -> Act -> Reflect loop in action, orchestrated durably by Trigger.dev.
Mini-Challenge: Enhance Your Agent with Human-in-the-Loop
Your challenge is to add a human approval step before the publishSummaryTool is called. If the human rejects the summary, the agent should try to regenerate it.
Challenge:
Modify the ai-smart-agent-with-tools job to include a human approval step.
- After the LLM generates a summary (but before
publishSummaryToolis called), useio.waitforExternalService(or a similar mechanism, perhaps by sending an event to a dedicated human-approval job) to prompt for human approval. - If approved, proceed to
publishSummaryTool. - If rejected, add a message to the
messagesarray indicating the rejection and the reason, and then let the agent loop again, giving the LLM a chance to regenerate a better summary based on the feedback.
Hint:
- You can simulate human approval by using
io.waitforExternalServicewith a custom event that you manually trigger from the dashboard (e.g.,human.approval). - The payload of this event could include
approved: booleanandfeedback: string. - Remember to add the human feedback back into the
messagesarray for the LLM to process.
What to observe/learn:
- How Trigger.dev’s durable execution allows you to pause a workflow for external human input.
- How the agent can adapt its behavior based on feedback, demonstrating a more sophisticated agentic loop.
Common Pitfalls & Troubleshooting for Agents
Building AI agents, especially with complex workflows, can introduce new challenges.
Non-Deterministic Behavior: LLMs are inherently probabilistic. An agent might take a different path or generate slightly different responses each time, even with the same input.
- Troubleshooting:
- Temperature: Lower the
temperatureparameter in your LLM calls (e.g., to 0.0 or 0.1) for more deterministic outputs, especially for critical decisions. - System Prompt: Refine your system prompt to be very explicit about expected behavior, output format, and decision-making logic.
- Logging: Use
io.logger.infoandio.logger.debugextensively to trace the exact messages sent to and received from the LLM, and the tool calls made. This is invaluable for understanding why an agent made a particular decision.
- Temperature: Lower the
- Troubleshooting:
Infinite Loops or Max Loop Count Reached: Agents can sometimes get stuck in a loop, repeatedly trying the same action or failing to progress.
- Troubleshooting:
MAX_LOOP_COUNT: Always implement a maximum loop count as a safeguard.- Reflection: Improve the agent’s “reflection” step. Teach the LLM to recognize when it’s stuck or making no progress and to ask for help or terminate.
- Tool Output Clarity: Ensure tool outputs are clear and concise. Ambiguous tool outputs can confuse the LLM.
- State Management: Explicitly track the agent’s internal state. If an agent keeps trying the same action, it might not be properly updating its understanding of the current situation.
- Troubleshooting:
Tool Invocation Failures & Rate Limits: External APIs (including LLM APIs) can fail, be slow, or impose rate limits.
- Troubleshooting:
- Trigger.dev Retries: Leverage Trigger.dev’s
retryoptions onio.runTaskfor transient errors. - Error Handling in Tools: Implement robust
try...catchblocks within your tool functions to catch API-specific errors and return meaningful error messages to the LLM. - Exponential Backoff: When implementing your own API calls within tools, use exponential backoff for retries to avoid overwhelming external services.
- Concurrency Limits: Trigger.dev can manage concurrency for your jobs, preventing you from hitting external API rate limits too aggressively from multiple concurrent agent runs.
- Trigger.dev Retries: Leverage Trigger.dev’s
- Troubleshooting:
📌 Key Idea: Observability is your best friend when debugging AI agents. The more detailed logs and traces you have (courtesy of Trigger.dev), the easier it is to understand an agent’s “thought process” and pinpoint where it went astray.
Summary
Congratulations! You’ve taken a significant step into the world of AI agents with Trigger.dev.
Here’s a recap of what we covered:
- AI Agents Defined: We understood agents as systems that Observe, Plan, Act, and Reflect to achieve goals.
- Trigger.dev’s Role: We learned how Trigger.dev’s durable execution, retries, and observability provide a robust foundation for building reliable, long-running agentic workflows.
- Agentic Job Creation: You set up a basic Trigger.dev job to orchestrate LLM calls.
- Tool Integration: We enhanced the agent by integrating external tools using OpenAI’s function calling, allowing the agent to interact with the outside world.
- Mini-Challenge: You were tasked with adding a human-in-the-loop approval step, further demonstrating Trigger.dev’s flexibility for complex workflows.
- Troubleshooting: We discussed common pitfalls like non-determinism, infinite loops, and tool failures, along with strategies for addressing them.
By combining the intelligence of LLMs with the reliability of Trigger.dev, you can build powerful, automated systems that go beyond simple scripts, tackling complex tasks with resilience and adaptability.
In the next chapter, we’ll delve deeper into advanced deployment strategies and how to effectively monitor your Trigger.dev applications in production.
References
- Trigger.dev Documentation
- Trigger.dev GitHub Repository
- OpenAI API Documentation
- OpenAI Function Calling Guide
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.