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:

flowchart TD A[Trigger.dev Workflow] --> B{Custom Connector Instance} B --> C[Connector Actions] B --> D[Connector Triggers] C --> E[External Service API] D --> E subgraph CD["Connector Definition"] F[ID and Name] G[Configuration Schemas] H[Authentication Secrets] end B --> F B --> G B --> H
  • 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/sdk that 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 an apiKey which must be a string. This apiKey will be provided by the user when they connect to the Simple Chat Service in 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 to defineConnector to add an action.
  • id and name: Unique identifier and human-readable name for this specific action.
  • input: A Zod schema defining the expected arguments for this action (e.g., channel and message). Trigger.dev will validate these inputs before executing run.
  • 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 our apiKey.
    • ctx: Provides useful utilities like a logger for debugging.
  • Simulated API Call: We’ve included a fetch call to https://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 how config.apiKey is used for authentication.
  • Error Handling: The try...catch block 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 import eventTrigger alongside client from our trigger.ts file.
  • 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 (our apiKey). In a real application, process.env.SIMPLE_CHAT_API_KEY would 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 via io.simpleChatService.
  • trigger: eventTrigger(...): We use eventTrigger to define a simple, manually callable trigger. The schema specifies that the event payload should contain channel and message properties.
  • io.simpleChatService.sendMessage(...): Inside the run function, 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 our sendMessage action’s schema. Notice how payload.message is consistently used here, matching the eventTrigger schema.

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 to defineAction, but for incoming events.
  • id and name: Identifiers for this specific trigger.
  • input: The Zod schema for the expected payload from the external service.
  • run: For triggers, the run function 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:

  1. Add another .defineAction() block after sendMessage.
  2. Define id, name, input (for channelId), and output (for channel info).
  3. Inside the run function, use setTimeout to delay the response and return a hardcoded object matching your output schema.

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 .env file or Trigger.dev secrets. Is it correct and does it have the necessary permissions?
      • Ensure your Authorization header 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 .env files that might not be deployed securely.
  • Schema Mismatches (Input/Output Validation):

    • Problem: Your action fails because the input payload doesn’t match your input Zod schema, or your run function returns data that doesn’t match your output Zod schema.
    • Debugging:
      • Carefully compare the data you’re passing to the action with the input schema you defined. The error message from Zod will usually pinpoint the exact field causing the issue.
      • Ensure the data returned by your run function (the actual return { ... } object) perfectly matches your output schema, 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.
  • 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 run function, 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 run function isn’t behaving as expected, but there are no obvious errors.
    • Debugging:
      • Use ctx.logger.info() and ctx.logger.error() extensively within your run function. 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.

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, schemas for configuration, and can include actions (for calling the external service) and triggers (for the external service to initiate workflows).
  • Step-by-Step Implementation: We learned how to define a connector using defineConnector, add actions with defineAction, specify input and output schemas, and implement the run function for actual API interaction.
  • Integration: Custom connectors are initialized and registered with a Trigger.dev job, making their actions available via the io object.
  • 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

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.