Sometimes it makes sense to be able to invoke a Job manually, without having to specify an event, especially for cases where you want to get notified when the invoked Job Run is complete.

To specify that a job is manually invokable, you can use the invokeTrigger() function when defining a job:

exampleJob.ts
import { invokeTrigger } from "@trigger.dev/sdk";
import { client } from "@/trigger";

export const exampleJob = client.defineJob({
  id: "example-job",
  name: "Example job",
  version: "1.0.1",
  trigger: invokeTrigger(),
  run: async (payload, io, ctx) => {
    // do something with the payload
  },
});

And then you can invoke the job using the Job.invoke() method:

example.ts
import { exampleJob } from "./exampleJob";

const jobRun = await exampleJob.invoke({ foo: "bar" });

Payload Schema

You can specify the type of the expected payload by passing a Zod schema to invokeTrigger():

exampleJob.ts
import { invokeTrigger } from "@trigger.dev/sdk";
import { client } from "@/trigger";

export const exampleJob = client.defineJob({
  id: "example-job",
  name: "Example job",
  version: "1.0.1",
  trigger: invokeTrigger({
    //the expected payload shape
    schema: z.object({
      userId: z.string(),
      tier: z.enum(["free", "pro"]),
    }),
  }),
  run: async (payload, io, ctx) => {
    // payload is typed as { userId: string, tier: "free" | "pro" }
  },
});

Now when you invoke the job, you will get a type error if the payload does not match the schema:

example.ts
import { exampleJob } from "./exampleJob";
// this will throw a type error because the payload does not match the schema
const jobRun = await exampleJob.invoke({ foo: "bar" });
// this will work
const jobRun = await exampleJob.invoke({ userId: "123", tier: "free" });

Invoking a Job

As you can see in the example above, invoking a job is as simple as calling the Job.invoke() method. This method returns a JobRun object that you can use to track the progress of the job run, especially in conjunction with our React hooks, like useRunDetails()

import { exampleJob } from "./exampleJob";

// Somewhere in your backend code
const jobRun = await exampleJob.invoke({ userId: "123", tier: "free" });

// Somewhere in your frontend code
const { data: jobRunDetails } = useRunDetails(jobRun.id);

There are many variations for how you could get the job run information passed from your backend to your frontend, depending on the framework you are using and which rendering model. We’ll leave that as an exercise for the reader

Deduplicate Invocations

You can pass an optional idempotencyKey to the invoke() method to deduplicate invocations. This is useful when you want to make sure that a job is only invoked once for a given payload.

import { exampleJob } from "./exampleJob";

// Somewhere in your backend code
const jobRun = await exampleJob.invoke(
  { userId: "123", tier: "free" },
  { idempotencyKey: "abc123" }
);

// This will not invoke the job again, but return the existing job run
const jobRun2 = await exampleJob.invoke(
  { userId: "123", tier: "free" },
  { idempotencyKey: "abc123" }
);

Callback URL

You can also pass an optional callbackUrl to the invoke() method to get notified when the job run is complete, either successfully or with an error.

import { exampleJob } from "./exampleJob";

// Somewhere in your backend code
const jobRun = await exampleJob.invoke(
  { userId: "123", tier: "free" },
  { callbackUrl: `${process.env.VERCEL_URL}/api/trigger/runs` }
);

When the run is complete, we will issue a POST request to the URL with the RunNotification payload.

Verifying the callback

You should make sure to verify the payload in your callback route to make sure that the request is coming from Trigger. You can do this by checking the X-Trigger-Signature-256 header, which contains a HMAC signature of the payload using your secret API Key.

import { crypto } from "node:crypto"

app.post("/api/trigger/runs", (req, res) => {
  // Create digest with payload + hmac secret
  const hashPayload = req.rawBody;
  const hmac = crypto.createHmac("sha256", process.env.TRIGGER_API_KEY); /
  const digest = Buffer.from(
    signatureAlgorithm + "=" + hmac.update(hashPayload).digest("hex"),
    "utf8"
  );

  // Get hash sent by the provider
  const providerSig = Buffer.from(req.get("X-Trigger-Signature-256") || "", "utf8");

  // Compare digest signature with signature sent by provider
  if (providerSig.length !== digest.length || !crypto.timingSafeEqual(digest, providerSig)) {
    res.status(401).send("Unauthorized");
  } else {
    // Webhook Authenticated
    // process and respond...
    res.json({ message: "Success" });
  }
});

Additional context

You can pass an optional context object to the invoke() method, which will be available in the job run context. This is useful for passing additional information to the job run that doesn’t make sense in the payload.

import { exampleJob } from "./exampleJob";

// Somewhere in your backend code
const jobRun = await exampleJob.invoke(
  { userId: "123", tier: "free" },
  {
    context: {
      traceId: "trace_123",
      correlationId: "def456",
    },
  }
);

And then you can access the context in the job run:

export const exampleJob = client.defineJob({
  id: "example-job",
  name: "Example job",
  version: "1.0.1",
  trigger: invokeTrigger({
    //the expected payload shape
    schema: z.object({
      userId: z.string(),
      tier: z.enum(["free", "pro"]),
    }),
  }),
  run: async (payload, io, ctx) => {
    console.log(ctx.event.context); // { traceId: "trace_123", correlationId: "def456" }
  },
});

Invoking a job from a job

You can also invoke a job from another job:

import { exampleJob } from "./exampleJob";

client.defineJob({
  id: "example-job2",
  name: "Example job 2",
  version: "1.0.1",
  trigger: intervalTrigger({
    seconds: 60,
  }),
  run: async (payload, io, ctx) => {
    const jobRun = await exampleJob.invoke("⚡", { userId: "123", tier: "free" });
  },
});

Notice how the invoke() method takes a string as the first argument. This is because under the hood invoke() is automatically creating a Task and the "⚡" string is the cacheKey for the created task. You can easily see the run created via the Run Dashboard:

Task

If Job.invoke() is called within another job, and you don’t include the cacheKey we’ll throw an error.

Wait for completion

You can also invoke a job and wait for it to complete before continuing execution of the current job:

import { exampleJob } from "./exampleJob";

client.defineJob({
  id: "example-job2",
  name: "Example job 2",
  version: "1.0.1",
  trigger: intervalTrigger({
    seconds: 60,
  }),
  run: async (payload, io, ctx) => {
    const jobRun = await exampleJob.invokeAndWaitForCompletion("⚡", {
      userId: "123",
      tier: "free",
    });

    if (jobRun.ok) {
      // The job run finished successfully
      console.log(jobRun.output);
    } else {
      // The job run finished with an error
      console.log(`The job failed with status ${jobRun.status}`, jobRun.error);
    }
  },
});

By default, invokeAndWaitForCompletion() will wait for the job run to complete for up to 60 minutes. You can change this by passing a timeoutInSeconds option:

import { exampleJob } from "./exampleJob";

client.defineJob({
  id: "example-job2",
  name: "Example job 2",
  version: "1.0.1",
  trigger: intervalTrigger({
    seconds: 60,
  }),
  run: async (payload, io, ctx) => {
    const jobRun = await exampleJob.invokeAndWaitForCompletion(
      "⚡",
      {
        userId: "123",
        tier: "free",
      },
      86_400 // 24 hours
    );

    if (jobRun.ok) {
      // The job run finished successfully
      console.log(jobRun.output);
    } else {
      // The job run finished with an error
      console.log(`The job failed with status ${jobRun.status}`, jobRun.error);
    }
  },
});

If the invoked job run doesn’t complete in the given time, the underlying task will fail, along with the run

The return value of invokeAndWaitForCompletion is a RunNotification object.

Batch invoke and wait for completion

You can also batch invoke jobs and wait for them all to complete before continuing execution of the current job:

import { exampleJob } from "./exampleJob";

client.defineJob({
  id: "example-job2",
  name: "Example job 2",
  version: "1.0.1",
  trigger: intervalTrigger({
    seconds: 60,
  }),
  run: async (payload, io, ctx) => {
    const runs = await exampleJob.batchInvokeAndWaitForCompletion("⚡", [
      {
        payload: {
          userId: "123",
          tier: "free",
        },
        timeoutInSeconds: 86_400, // 24 hours
      },
      {
        payload: {
          userId: "abc",
          tier: "paid",
        },
        timeoutInSeconds: 86_400, // 24 hours
      },
    ]);

    // runs is an array of RunNotification objects
  },
});

You can batch up to 25 invocations at once, and we will run them in parallel and wait for all of them to complete before continuing execution of the current job.

You can in an optional options object to each invocation, if you want to pass context and accountId to each invocation:

import { exampleJob } from "./exampleJob";

client.defineJob({
  id: "example-job2",
  name: "Example job 2",
  version: "1.0.1",
  trigger: intervalTrigger({
    seconds: 60,
  }),
  run: async (payload, io, ctx) => {
    const runs = await exampleJob.batchInvokeAndWaitForCompletion("⚡", [
      {
        payload: {
          userId: "123",
          tier: "free",
        },
        timeoutInSeconds: 86_400, // 24 hours
        options: {
          context: {
            traceId: "trace_123",
            correlationId: "def456",
          },
          accountId: "abc123",
        },
      },
      {
        payload: {
          userId: "abc",
          tier: "paid",
        },
        timeoutInSeconds: 86_400, // 24 hours
        options: {
          context: {
            traceId: "trace_abc",
            correlationId: "defabc",
          },
          accountId: "abc123",
        },
      },
    ]);

    // runs is an array of RunNotification objects
  },
});