Pre-requisites

This guide assumes you already have a project setup and working with Trigger.dev. If not, head over to our Quick Start guides to get up and running in a few minutes.

1. Define your Job

A Job is a collection of Tasks that are run in a specific order. You can think of it as a function that you can run on a schedule, or when an event happens. Jobs are defined by calling the TriggerClient.defineJob function

//this path might be different depending on your project
import { client } from "@/trigger";

client.defineJob({
  // ... job definition
});

If you aren’t seeing defined jobs in your Trigger.dev dashboard, it might be because the job file isn’t being imported in your app.

2. Choose a name and ID

Each job must have a unique and stable id and name. The id is used to identify the Job in the database, and the name is used to identify the Job in the UI.

client.defineJob({
  id: "my-job",
  name: "My Job",
  // ... job definition
});

We will pass the value of the id property through a slugifier because we use it in URLs in our Dashboard. This means you can use any characters you want, but we recommend using only lowercase letters, numbers and dashes.

3. Set the current version

The version property is used to track changes to your Job. It’s required to be a semantic version string. You can track changes to your Job by incrementing the version number, and we will display in the Dashboard which version each Job Run was created with.

client.defineJob({
  id: "my-job",
  name: "My Job",
  version: "1.0.0",
  // ... job definition
});

4. Choose a Trigger

The trigger you choose determines how and when a job will run. See our Triggers guide for more information. The Trigger you choose also defines the type of the run payload argument (more in this below)

client.defineJob({
  id: "my-job",
  name: "My Job",
  version: "1.0.0",
  trigger: eventTrigger({
    name: "my.event",
  }),
  // ... job definition
});

5. Add integrations (optional)

Integrations provide a convienent way to create and run tasks against authenticated APIs inside your Job’s run function. You’ll need to pass them in the integrations option when defining your Job.

import { Slack } from "@trigger.dev/slack";

const slack = new Slack({ id: "slack" });

client.defineJob({
  id: "my-job",
  name: "My Job",
  version: "1.0.0",
  trigger: eventTrigger({
    name: "my.event",
  }),
  integrations: { slack },
  // ... job definition
});

6. Implement the run function

The run function implements your custom code that will be executed when your Job is run. It’s an async function that takes three arguments:

  • The run payload - The type of the payload argument is determined by the Trigger you choose.
  • An instance of IO, which exposes built-in tasks and allows you to create your own, as well as interact with integrations.
  • A context object, which contains information about the current run, such as the run ID, the Job ID, and the Job version. Context reference
import { Slack } from "@trigger.dev/slack";

const slack = new Slack({ id: "slack" });

client.defineJob({
  id: "my-job",
  name: "My Job",
  version: "1.0.0",
  trigger: eventTrigger({
    name: "my.event",
  }),
  integrations: { slack },
  run: async (payload, io, context) => {
    // ... your code
  },
});

We do not compile and ship your code to run on the Trigger.dev server. It runs exactly where you’ve deployed your code (e.g. Vercel).

The run function you define is like a normal JavaScript function in all respects except one: it will be called one or more times to complete a single Job Run.

This means that you can’t rely on any state that is not persisted between runs, and you must create tasks to ensure that work is not repeated.

Tasks are so important that we’ve dedicated a whole section to them. See our Tasks guide for more information.

Built in tasks

We provide some built-in tasks that you can use in your run function:

TaskDescriptionTask code
DelayWait for a period of timeawait io.wait("wait", 60);
LogLog a messageawait io.logger.log("Hello");
Send EventSend an event (for eventTrigger)await io.sendEvent("my-event", { name: "my.event", payload: { hello: "world" } });
Background fetchFetch data from a URL that can take longer that the serverless timeout.await io.backgroundFetch("fetch-some-data", { url: "https://example.com" });

For a full list of built-in Tasks, see the io SDK reference. The below example makes use a few of these built-in tasks:

import { Slack } from "@trigger.dev/slack";

const slack = new Slack({ id: "slack" });

client.defineJob({
  id: "my-job",
  name: "My Job",
  version: "1.0.0",
  trigger: eventTrigger({
    name: "my.event",
  }),
  // Optional integration
  integrations: { slack },
  run: async (payload, io, context) => {
    await io.logger.info("Received the my.event event", { payload });
    await io.sendEvent("send-event", {
      name: "other.event",
      payload: { hello: "world" },
    });

    await io.wait("wait for 60 seconds", 60);

    await io.backgroundFetch("fetch-some-data", {
      url: "https://example.com",
    });
  },
});

Create your own tasks

You can also create your own tasks or use tasks provided by our integration packages. See Creating Tasks for more information. The example below demonstrates creating tasks in 3 different ways:

import { Slack } from "@trigger.dev/slack";

const slack = new Slack({ id: "slack" });

client.defineJob({
  id: "my-job",
  name: "My Job",
  version: "1.0.0",
  trigger: eventTrigger({
    name: "my.event",
  }),
  // Optional integration
  integrations: { slack },
  run: async (payload, io, context) => {
    // Use runTask with the "get-user" cacheKey, and return the user
    const user = await io.runTask("get-user", async () => {
      return prisma.user.findUniqueOrThrow({
        where: {
          id: payload.id,
        },
      });
    });

    // Use the Slack integration to create a task using the "post-message" cacheKey
    const message = await io.slack.postMessage("post-message", {
      channel: process.env.SLACK_CHANNEL_ID,
      message: `Hello ${user.name}`,
    });

    await io.wait("wait for 10 seconds", 10);

    // Use the Slack integration's runTask method to add a reaction to the message
    await io.slack.runTask("add-reaction", async (client) => {
      // client here is an authenticated instance of the Slack SDK
      await client.reactions.add({
        channel: process.env.SLACK_CHANNEL_ID,
        name: "thumbsup",
        timestamp: message.ts,
      });
    });
  },
});

7. Handling errors

If your run function throws an error, the Job Run will fail and the error will be displayed in the Dashboard. If you’d like to retry on an error, you can do so using tasks and the retry option.

const user = await io.runTask(
  "get-user",
  async () => {
    return prisma.user.findUniqueOrThrow({
      where: {
        id: payload.id,
      },
    });
  },
  {
    retry: {
      limit: 3,
      factor: 2,
      minTimeoutInMs: 1000,
    },
  }
);

See the retry options for more information.

8. Skip catching internal errors

We will throw some errors internally to interrupt run execution so they can be resumed later. If you put a try/catch block in your run code and catch these errors, your job will not work correctly. You can check if an error is an internal error using isTriggerError():

client.defineJob({
  run: async (payload, io, context) => {
    try {
      // Use runTask with the "get-user" cacheKey, and return the user
      const user = await io.runTask("get-user", async () => {
        return prisma.user.findUniqueOrThrow({
          where: {
            id: payload.id,
          },
        });
      });
    } catch (error) {
      if (isTriggerError(error)) throw error;

      // do something with your error here
    }
  },
});

Alternatively, you can use the io.try() function:

client.defineJob({
  run: async (payload, io, context) => {
    const result = io.try(
      () => {
        return io.runTask("get-user", async () => {
          return prisma.user.findUniqueOrThrow({
            where: {
              id: payload.id,
            },
          });
        });
      },
      async (error) => {
        //you can return data from the error handler,
        //if you wish to elegantly deal with errors
        return {
          success: false as const,
          error,
        };
      }
    );
  },
});

9. Creating tasks in a loop

If you want to create tasks in a loop, you should use for const ... of to ensure that the tasks are created in the correct order.

client.defineJob({
  run: async (payload, io, context) => {
    for (const user of payload.users) {
      await io.runTask(`update-user-${user.id}`, async () => {
        return prisma.user.update({
          where: {
            id: user.id,
          },
          data: {
            name: user.name,
          },
        });
      });
    }
  },
});

We don’t currently support running tasks in parallel so Promise.all will not work correctly. This is something on our roadmap that we hope to support soon.

10. Return data from your Job

Anything you return from the run function will be automatically set as the run output and displayed in the Dashboard.

client.defineJob({
  run: async (payload, io, context) => {
    return {
      success: true,
      message: "Hello world",
    };
  },
});

Next steps

We recommend exploring all of the below sections to fully understand how to create and run Jobs using Trigger.dev.