Introduction
As you build increasingly sophisticated AI agents and automated workflows, you’ll inevitably encounter the need to connect to a wider array of services than any platform can offer out-of-the-box. This is where advanced integrations become crucial. You might need to interact with a niche third-party API, a legacy internal system, or perhaps a highly specialized AI model hosted in a unique environment.
This chapter dives into how Trigger.dev empowers you to go beyond its standard integrations. We’ll explore the concept of the Managed Connector Platform (MCP) and, more importantly, guide you through building your own custom connectors. Mastering this skill allows your Trigger.dev workflows to truly become the central nervous system for all your operations, regardless of how obscure or proprietary your external services might be.
To get the most out of this chapter, you should be comfortable with basic Trigger.dev workflow creation, understanding of event-driven architectures, and have a foundational grasp of API interactions and TypeScript. We’re building on the knowledge gained in previous chapters, so prepare to expand your horizons!
Core Concepts: Extending Trigger.dev’s Reach
Trigger.dev provides a robust foundation for orchestrating workflows and AI agents. But its true power lies in its ability to seamlessly integrate with virtually any external service. This is achieved through its connector ecosystem, which includes both a Managed Connector Platform and the flexibility to create custom connectors.
The Managed Connector Platform (MCP)
What if you could connect to dozens of popular SaaS applications without worrying about API keys, authentication flows, rate limits, or error handling? That’s precisely the problem the Managed Connector Platform (MCP) solves.
What it is: The Trigger.dev Managed Connector Platform (MCP) is a system that provides and manages a collection of pre-built, production-ready integrations with popular third-party services. Think of it as a curated library of “smart adapters” for common APIs like Stripe, GitHub, Slack, OpenAI, and more. These are often developed and maintained by the Trigger.dev team or trusted partners.
Why it exists: Integrating with external APIs is often tedious. Each service has its own authentication method (API keys, OAuth, etc.), rate limiting, error codes, and data structures. The MCP abstracts away this complexity, offering a standardized, simplified interface within your Trigger.dev workflows. This significantly reduces development time and the potential for integration-specific bugs.
How it works: When you use an MCP-provided connector (e.g., github.runsTask), Trigger.dev handles the underlying HTTP requests, authentication (using secrets you provide), retries for transient errors, and often transforms the data into a consistent format. You simply call a function, and the MCP ensures it reaches the external service reliably.
⚡ Real-world insight: In a production system, relying on MCP connectors for common services means your team can focus on core business logic rather than building and maintaining dozens of API clients. It’s a huge time-saver and reliability booster.
The Need for Custom Connectors
While the MCP covers many popular services, the world of software is vast. You’ll inevitably encounter scenarios where a pre-built connector doesn’t exist or doesn’t precisely fit your needs.
When built-in isn’t enough:
- Proprietary Internal Systems: Your company might have its own APIs for CRM, ERP, or custom data stores that are not publicly exposed.
- Niche Third-Party Services: You might be working with a highly specialized vendor API that isn’t widely adopted enough for an MCP connector.
- Unique Data Transformations: You might need to perform specific data manipulation before or after interacting with an API, which a generic connector might not support.
- Custom Authentication Flows: Some services require complex multi-step authentication that differs from standard API key or OAuth patterns.
Benefits of Custom Connectors:
- Tailored Logic: You have full control over the interaction, allowing for highly specific requests and responses.
- Seamless Integration: Your proprietary systems can become first-class citizens in your Trigger.dev workflows.
- Enhanced Security: You control how secrets are used and transmitted, aligning with internal security policies.
- Future-Proofing: You can adapt your connector as the external API evolves, without waiting for a platform update.
Anatomy of a Custom Trigger.dev Connector
A custom Trigger.dev connector is essentially a TypeScript module that defines how your workflows can interact with an external service. It typically consists of:
- ID and Name: Unique identifiers for your connector.
- Schemas: Define the configuration options and authentication secrets needed for the connector.
- Actions: Functions that perform specific operations (e.g.,
createUser,sendNotification,fetchData). These are the methods your workflows will call. - Triggers (Optional): Mechanisms for the external service to initiate a Trigger.dev workflow (e.g., via webhooks).
Let’s visualize the core components of a custom connector:
- Trigger.dev Workflow: Your actual workflow code that orchestrates tasks.
- Custom Connector Instance: How your workflow interacts with your custom connector.
- Connector Actions: Functions within your connector that perform operations on the external service.
- Connector Triggers: How events from the external service can start your Trigger.dev workflows.
- External Service API: The actual API endpoint of the service you’re integrating with.
- Connector Definition: The metadata and configuration details that describe your connector.
Building a Custom Connector: A Step-by-Step Guide
For this guide, we’ll create a simple custom connector for a hypothetical “Simple Chat Service” that allows us to send messages.
Step 1: Project Setup
We’ll assume you already have a Trigger.dev project initialized. If not, make sure to use v4-beta. As of 2026-05-20, v3 is the current stable, but v4 is expected to go GA around May/June 2026.
npx trigger.dev@v4-beta init
Inside your Trigger.dev project, navigate to your src directory. We’ll create a new folder for our custom connectors.
# In your project root
mkdir -p src/connectors/simple-chat
touch src/connectors/simple-chat/index.ts
This creates a dedicated directory for our simple-chat connector and an index.ts file where its logic will reside.
Step 2: Defining the Connector Interface
First, we need to define the basic structure of our connector using Trigger.dev’s defineConnector helper. This includes its id, name, and an optional schemas object for configuration.
Open src/connectors/simple-chat/index.ts and add the following:
// src/connectors/simple-chat/index.ts
import { defineConnector } from "@trigger.dev/sdk";
import { z } from "zod"; // We'll use Zod for schema validation
// Define the configuration schema for our connector
// This allows users to provide an API key when setting up the connector
const SimpleChatSchema = z.object({
apiKey: z.string(), // A string for our API key
// We could add more config options here, like a default channel ID
});
export const simpleChat = defineConnector({
id: "simple-chat", // Unique ID for the connector
name: "Simple Chat Service", // Human-readable name
version: "1.0.0", // Version of your connector
schemas: {
// This schema will be used to validate the config object when the connector is initialized
// It also helps Trigger.dev understand what secrets/config the connector needs
config: SimpleChatSchema,
},
});
Explanation:
defineConnector: This is the core function from@trigger.dev/sdkthat helps you build a connector.zod: A TypeScript-first schema declaration and validation library. Trigger.dev uses Zod internally for schema validation, making it a natural fit for defining connector configurations.SimpleChatSchema: Defines that our connector needs anapiKeywhich must be a string. ThisapiKeywill be provided by the user when they connect to theSimple Chat Servicein their Trigger.dev dashboard or directly in code.id: A unique identifier for your connector (e.g.,simple-chat).name: A user-friendly name that will appear in the Trigger.dev UI.version: A semantic version for your connector, useful for managing updates.schemas.config: This tells Trigger.dev what configuration properties your connector expects.
Step 3: Implementing Actions
Actions are the core functionality of your connector. They define what operations your workflows can perform on the external service. Let’s add an action to send a message.
Update src/connectors/simple-chat/index.ts:
// src/connectors/simple-chat/index.ts
import { defineConnector } from "@trigger.dev/sdk";
import { z } from "zod";
const SimpleChatSchema = z.object({
apiKey: z.string(),
});
export const simpleChat = defineConnector({
id: "simple-chat",
name: "Simple Chat Service",
version: "1.0.0",
schemas: {
config: SimpleChatSchema,
},
}).defineAction({ // We're adding an action here!
id: "sendMessage", // Unique ID for this action
name: "Send Message", // Human-readable name for the action
input: z.object({ // Schema for the input parameters of this action
channel: z.string(),
message: z.string(),
}),
output: z.object({ // Schema for the output (return value) of this action
success: z.boolean(),
messageId: z.string().optional(),
error: z.string().optional(),
}),
run: async (payload, { config, ctx }) => {
// This is the actual function that runs when the action is called
// `payload` contains the input defined in `input` schema
// `config` contains the connector's configuration (our apiKey)
// `ctx` provides context like logger
ctx.logger.info(`Sending message to channel ${payload.channel}`);
try {
// In a real scenario, you'd make an HTTP request to your Simple Chat Service here
// For this example, we'll simulate an API call
const response = await fetch("https://api.simplechat.com/v1/message", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${config.apiKey}`, // Use the API key from the config
},
body: JSON.stringify({
channel: payload.channel,
text: payload.message,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to send message: ${response.status} - ${errorData.message}`);
}
const data = await response.json();
return { success: true, messageId: data.id };
} catch (error) {
ctx.logger.error("Error sending message:", error);
return { success: false, error: (error as Error).message };
}
},
});
Explanation:
.defineAction(): This method is chained todefineConnectorto add an action.idandname: Unique identifier and human-readable name for this specific action.input: A Zod schema defining the expected arguments for this action (e.g.,channelandmessage). Trigger.dev will validate these inputs before executingrun.output: A Zod schema defining the expected return value of this action. This helps with type safety and understanding what data you’ll get back.run: async (payload, { config, ctx }) => { ... }: This is the asynchronous function that executes the action.payload: An object containing the validated input parameters (channel,message).config: An object containing the connector’s configuration, which includes ourapiKey.ctx: Provides useful utilities like aloggerfor debugging.
- Simulated API Call: We’ve included a
fetchcall tohttps://api.simplechat.com/v1/message. In a real application, you would replace this with the actual API endpoint and client logic for your external service. Notice howconfig.apiKeyis used for authentication. - Error Handling: The
try...catchblock ensures that network errors or API errors are caught and returned gracefully.
Step 4: Integrating the Custom Connector into a Workflow
Now that our simpleChat connector is defined, we can use its sendMessage action in any Trigger.dev workflow.
First, make sure to install zod if you haven’t already:
npm install zod
# or
yarn add zod
Next, open one of your workflow files (e.g., src/jobs/my-first-workflow.ts) and modify it to use the new connector.
// src/jobs/my-first-workflow.ts
import { client, eventTrigger } from "../trigger"; // Ensure eventTrigger is imported
import { simpleChat } from "../connectors/simple-chat"; // Import our custom connector
import { z } from "zod"; // Import z for schema definition
// Initialize the custom connector instance
// You would typically store your API key securely, e.g., in environment variables
// For local development, you can put it in a .env file or directly here for testing.
// In production, use Trigger.dev's secret management.
const simpleChatService = simpleChat.init({
apiKey: process.env.SIMPLE_CHAT_API_KEY || "your_dev_api_key_here",
});
client.defineJob({
id: "send-chat-message",
name: "Send Chat Message to Custom Service",
version: "1.0.0",
integrations: {
simpleChatService, // Register our custom connector instance
},
trigger: eventTrigger({ // Using eventTrigger for a straightforward manual test
name: "manual.send.message",
schema: z.object({
channel: z.string(),
message: z.string(), // The payload will have a 'message' field
}),
}),
run: async (payload, io, ctx) => {
// Use the custom connector's action
const result = await io.simpleChatService.sendMessage(
"send-message-action", // Unique key for this step
{
channel: payload.channel,
message: payload.message, // Use payload.message from the trigger
}
);
if (result.success) {
io.logger.info(`Message sent successfully! Message ID: ${result.messageId}`);
} else {
io.logger.error(`Failed to send message: ${result.error}`);
throw new Error(`Chat service error: ${result.error}`);
}
return { status: "message_processed", messageId: result.messageId };
},
});
Explanation:
import { client, eventTrigger } from "../trigger";: We importeventTriggeralongsideclientfrom ourtrigger.tsfile.import { simpleChat } from "../connectors/simple-chat";: We import our defined connector.simpleChat.init({ apiKey: ... }): We initialize an instance of our connector, providing the necessary configuration (ourapiKey). In a real application,process.env.SIMPLE_CHAT_API_KEYwould fetch the key from your environment variables, which is the recommended way to handle secrets.integrations: { simpleChatService, }: We register our initialized connector instance with the job. This makes it available viaio.simpleChatService.trigger: eventTrigger(...): We useeventTriggerto define a simple, manually callable trigger. Theschemaspecifies that the event payload should containchannelandmessageproperties.io.simpleChatService.sendMessage(...): Inside therunfunction, we call our custom action. The first argument is a unique key for this step (important for observability), and the second is the input payload matching oursendMessageaction’s schema. Notice howpayload.messageis consistently used here, matching theeventTriggerschema.
Step 5: Implementing Triggers (Conceptual)
While actions allow your workflows to call external services, triggers allow external services to start your workflows. Implementing a trigger for a custom connector involves defining how Trigger.dev listens for events from your external service. This is often done via webhooks.
In your src/connectors/simple-chat/index.ts, you could extend defineConnector with a defineTrigger block.
// ... existing code ...
export const simpleChat = defineConnector({
id: "simple-chat",
name: "Simple Chat Service",
version: "1.0.0",
schemas: {
config: SimpleChatSchema,
},
})
.defineAction({
// ... sendMessage action ...
})
.defineTrigger({ // Adding a trigger!
id: "message.received", // Unique ID for this trigger
name: "Message Received", // Human-readable name
input: z.object({ // Schema for the payload the external service sends
channel: z.string(),
sender: z.string(),
text: z.string(),
timestamp: z.string().datetime(),
}),
// For a real webhook trigger, you'd configure how Trigger.dev receives and validates it.
// This might involve a special endpoint URL provided by Trigger.dev for your connector,
// and your external service would send POST requests to it.
// The 'run' function here would typically just return the validated payload.
// The actual webhook endpoint creation and listening is handled by Trigger.dev internally
// when you define a trigger like this.
run: async (payload, { ctx }) => {
ctx.logger.info(`Received new message from Simple Chat Service: ${payload.text}`);
return payload; // Return the payload to be used by the workflow
},
});
Explanation:
.defineTrigger(): Similar todefineAction, but for incoming events.idandname: Identifiers for this specific trigger.input: The Zod schema for the expected payload from the external service.run: For triggers, therunfunction typically just validates and returns the incoming payload, allowing the workflow to process it. The magic of listening for HTTP requests (webhooks) is handled by Trigger.dev when you deploy this connector.
⚡ Quick Note: Setting up a real webhook for a custom service often requires configuring that service to send POST requests to a specific URL provided by Trigger.dev. This URL is generated when your Trigger.dev project is deployed and your connector is registered.
Mini-Challenge: Extend Your Custom Connector
Now it’s your turn to expand the functionality of our simpleChat connector!
Challenge: Add a new action to the simpleChat connector called getChannelInfo. This action should take a channelId (string) as input and return mock information about that channel, such as name (string), memberCount (number), and topic (string). Simulate an API call with a setTimeout to mimic network latency.
Hint:
- Add another
.defineAction()block aftersendMessage. - Define
id,name,input(forchannelId), andoutput(for channel info). - Inside the
runfunction, usesetTimeoutto delay the response and return a hardcoded object matching youroutputschema.
What to observe/learn: You’ll practice defining inputs and outputs for actions, simulating asynchronous operations, and extending the capabilities of your custom connector.
// Add this inside src/connectors/simple-chat/index.ts,
// after the sendMessage action and before any other code.
.defineAction({
id: "getChannelInfo",
name: "Get Channel Information",
input: z.object({
channelId: z.string(),
}),
output: z.object({
name: z.string(),
memberCount: z.number(),
topic: z.string().optional(),
found: z.boolean(),
}),
run: async (payload, { config, ctx }) => {
ctx.logger.info(`Fetching info for channel ID: ${payload.channelId}`);
// Simulate an API call with latency
await new Promise(resolve => setTimeout(resolve, 500)); // 500ms delay
if (payload.channelId === "general") {
return {
name: "General Chat",
memberCount: 123,
topic: "Discussions about everything!",
found: true,
};
} else if (payload.channelId === "dev-team") {
return {
name: "Development Team",
memberCount: 15,
topic: "Daily standups and coding questions.",
found: true,
};
} else {
return {
name: `Channel ${payload.channelId}`,
memberCount: 0,
topic: undefined,
found: false,
};
}
},
});
Once you’ve added this action, try calling it from a new job or within your existing send-chat-message job to verify it works!
// Example of using the new action in a job
// src/jobs/get-channel-info-job.ts (create a new file for this job)
import { client, eventTrigger } from "../trigger";
import { simpleChat } from "../connectors/simple-chat";
import { z } from "zod";
// Initialize the custom connector instance
const simpleChatService = simpleChat.init({
apiKey: process.env.SIMPLE_CHAT_API_KEY || "your_dev_api_key_here",
});
client.defineJob({
id: "get-chat-channel-info",
name: "Get Custom Chat Channel Info",
version: "1.0.0",
integrations: {
simpleChatService,
},
trigger: eventTrigger({
name: "manual.get.channel.info",
schema: z.object({
channelId: z.string(),
}),
}),
run: async (payload, io, ctx) => {
const channelInfo = await io.simpleChatService.getChannelInfo(
"get-channel-info-step",
{ channelId: payload.channelId }
);
if (channelInfo.found) {
io.logger.info(`Channel Name: ${channelInfo.name}`);
io.logger.info(`Members: ${channelInfo.memberCount}`);
if (channelInfo.topic) {
io.logger.info(`Topic: ${channelInfo.topic}`);
}
} else {
io.logger.warn(`Channel with ID ${payload.channelId} not found.`);
}
return channelInfo;
},
});
Common Pitfalls & Troubleshooting
Building custom integrations can sometimes be tricky. Here are a few common issues and how to approach them:
Authentication Failures:
- Problem: Your custom connector fails with 401 Unauthorized or 403 Forbidden errors when interacting with the external service.
- Debugging:
- Double-check your
apiKey(or other credentials) in your.envfile or Trigger.dev secrets. Is it correct and does it have the necessary permissions? - Ensure your
Authorizationheader format is correct (e.g.,Bearer YOUR_KEY,Basic BASE64_ENCODED_CREDENTIALS). - Verify the API key hasn’t expired or been revoked on the external service’s dashboard.
- ⚡ Real-world insight: Always use Trigger.dev’s secret management for production API keys, not hardcoded values or
.envfiles that might not be deployed securely.
- Double-check your
Schema Mismatches (Input/Output Validation):
- Problem: Your action fails because the input
payloaddoesn’t match yourinputZod schema, or yourrunfunction returns data that doesn’t match youroutputZod schema. - Debugging:
- Carefully compare the data you’re passing to the action with the
inputschema you defined. The error message from Zod will usually pinpoint the exact field causing the issue. - Ensure the data returned by your
runfunction (the actualreturn { ... }object) perfectly matches youroutputschema, including handling optional fields (.optional()). - Trigger.dev’s runtime will provide helpful error messages indicating where schema validation failed, often with a clear path to fix.
- Carefully compare the data you’re passing to the action with the
- Problem: Your action fails because the input
Rate Limiting from External APIs:
- Problem: Your connector works for a few calls, then starts failing with 429 Too Many Requests errors from the external API.
- Debugging:
- Check the external API’s documentation for specific rate limits and recommended retry strategies.
- Implement exponential backoff and retries within your
runfunction, or rely on Trigger.dev’s built-in retry mechanisms for the job step. - Consider caching results for frequently requested data that doesn’t change often.
- 🔥 Optimization / Pro tip: Trigger.dev’s durable execution can handle retries automatically for job steps. If the rate limit is hit, the job step will pause and retry later without consuming more of your current quota, making your workflows resilient.
Debugging Connector Logic:
- Problem: Your
runfunction isn’t behaving as expected, but there are no obvious errors. - Debugging:
- Use
ctx.logger.info()andctx.logger.error()extensively within yourrunfunction. These logs will appear in the Trigger.dev dashboard for the specific job run, providing crucial insights into execution flow and variable states. - During local development, you can use
console.log()to see output directly in your terminal. - Break down complex logic into smaller, testable functions to isolate potential issues.
- Use
- Problem: Your
Summary
In this chapter, we’ve taken a significant leap forward in understanding the power and flexibility of Trigger.dev’s integration capabilities.
Here are the key takeaways:
- MCP (Managed Connector Platform): Provides ready-to-use integrations for popular services, abstracting away API complexity and streamlining development.
- Custom Connectors: Essential for integrating with proprietary, niche, or highly customized external services, offering complete control over API interactions.
- Connector Anatomy: Custom connectors are defined by an
id,name,schemasfor configuration, and can includeactions(for calling the external service) andtriggers(for the external service to initiate workflows). - Step-by-Step Implementation: We learned how to define a connector using
defineConnector, add actions withdefineAction, specifyinputandoutputschemas, and implement therunfunction for actual API interaction. - Integration: Custom connectors are initialized and registered with a Trigger.dev job, making their actions available via the
ioobject. - Troubleshooting: Common issues like authentication failures, schema mismatches, and rate limiting can be resolved through careful configuration, robust logging, and leveraging Trigger.dev’s retry mechanisms.
You now have the knowledge to extend Trigger.dev’s reach to virtually any API, making your workflows incredibly powerful and adaptable. In the next chapter, we’ll delve into deploying your Trigger.dev projects to production environments, ensuring your robust workflows and custom integrations are ready for real-world traffic.
References
- Trigger.dev Documentation: https://trigger.dev/docs
- Trigger.dev GitHub Repository: https://github.com/triggerdotdev/trigger.dev
- Zod GitHub Repository: https://github.com/colinhacks/zod
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.