---
title: "Writing Jobs - Step by Step"
description: "Best practices for writing well-behaving Jobs in your codebase"
---

## 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](/documentation/quickstarts/introduction) 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

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

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

<Note>
  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.
</Note>

## 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.

```ts
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](https://semver.org/) 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.

```ts
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](/documentation/concepts/triggers/introduction) for more information. The Trigger you choose also defines the type of the run `payload` argument (more in this below)

```ts
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.

```ts
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](/sdk/context)

```ts
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
  },
});
```

<Note>
  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).
</Note>

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.

<Accordion title="Why does the run function work like this?">
  The run function is called multiple times to ensure that your Job is resilient to failure and can finish running even if it is interrupted. There are many reasons why a run could be interrupted and resumed later, including:

- The serverless function times out
- A task fails and needs to be retried
- A wait task is used to delay continuing the run until a later time
- A run yields execution to prevent the serverless function from timing out
- Waiting for a [backgroundFetch](/sdk/io/backgroundfetch) to complete
- Waiting for a task callback to be called (like the ones used in our [Replicate integration](/integrations/apis/replicate#predictions))
- Waiting for [another event](https://github.com/triggerdotdev/trigger.dev/issues/472) to fire.

</Accordion>

Tasks are so important that we've dedicated a whole section to them. See our [Tasks guide](/documentation/concepts/tasks) for more information.

### Built in tasks

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

| Task                                        | Description                                                             | Task code                                                                            |
| ------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| [Delay](/documentation/concepts/delays)     | Wait for a period of time                                               | `await io.wait("wait", 60);`                                                         |
| [Log](/sdk/io/logger)                       | Log a message                                                           | `await io.logger.log("Hello");`                                                      |
| [Send Event](/sdk/io/sendevent)             | Send an event (for eventTrigger)                                        | `await io.sendEvent("my-event", { name: "my.event", payload: { hello: "world" } });` |
| [Background fetch](/sdk/io/backgroundfetch) | Fetch 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](/sdk/io). The below example makes use a few of these built-in tasks:

```ts
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](/documentation/concepts/tasks#creating-tasks) for more information. The example below demonstrates creating tasks in 3 different ways:

```ts
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.

```ts
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](/sdk/io/runtask) 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()](/sdk/istriggererror):

```ts
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()](/sdk/io/try) function:

```ts
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.

```ts
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,
          },
        });
      });
    }
  },
});
```

<Warning>
  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.
</Warning>

## 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.

```ts
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.

<CardGroup cols={2}>
  <Card
    title="Running your jobs"
    icon="wand-magic-sparkles"
    href="/documentation/guides/running-jobs"
  >
  A guide for how to run your Jobs. Triggering a test Run and Triggering your Job for real

    </Card>

<Card title="Integrations" icon="grid-2" href="/integrations">
  Integrations make it easy to authenticate and use APIs. Learn how to use and create integrations.
</Card>
<Card title="API catalog with code samples" icon="code" href="https://trigger.dev/apis">
  Find code examples for many popular APIs. These can be copied / modified for use in your own
  projects.
</Card>
<Card title="SDK reference" icon="book-open" href="/sdk">
  How to use the SDK. This includes all the available Tasks, triggers and actions you can use.
</Card>

</CardGroup>
