| title | Trigger.dev integration |
|---|---|
| sidebar_label | Trigger.dev |
| description | Learn how to run Apify Actors from Trigger.dev tasks, suspend long runs until an Apify webhook resumes them, and give Trigger.dev AI agents Apify tools. |
| slug | /integrations/trigger-dev |
import ThirdPartyDisclaimer from '@site/sources/_partials/_third-party-integration.mdx';
Trigger.dev is an open-source platform for durable background tasks and AI agents. You write tasks in TypeScript instead of a visual builder, and Trigger.dev runs them with retries, queues, and run logs. With the Apify JavaScript client, a task can run any Actor from Apify Store, wait for the results, and pass them on to the rest of your code.
Trigger.dev's terms of service require a proxy for web scraping and prohibit scraping third-party websites directly from Trigger.dev Cloud without the site owner's permission, as its Puppeteer guide explains. Actors run on the Apify platform, so the scraping happens there and your task only calls the Apify API.
- An Apify account and an Apify API token
- A Trigger.dev project, set up as described in the Trigger.dev quick start
-
Install the Apify JavaScript client:
npm install apify-client
-
Add your token as
APIFY_TOKENto the.envfile for local development, and to the Environment Variables page in the Trigger.dev dashboard for deployed tasks:APIFY_TOKEN="<your-apify-token>" -
Add
apify-clienttobuild.externalintrigger.config.ts, so Trigger.dev doesn't bundle it:import { defineConfig } from "@trigger.dev/sdk"; export default defineConfig({ project: "<project ref>", // Your other config settings... build: { external: ["apify-client"], }, });
-
Create the Apify client that all the examples on this page share. The
x-apify-integration-platformheader tells Apify the requests come from Trigger.dev.import { ApifyClient } from "apify-client"; export const apify = new ApifyClient({ token: process.env.APIFY_TOKEN, requestInterceptors: [ (config) => { // Identifies requests from Trigger.dev tasks to Apify config.headers = { ...config.headers, "x-apify-integration-platform": "trigger.dev" }; return config; }, ], });
How long the Actor runs decides how your task should wait for it:
| Pattern | Use it for | While the Actor runs |
|---|---|---|
| Run an Actor and wait | Actors that finish in seconds | The task stays active and counts toward Trigger.dev compute usage. |
| Run an Actor with a webhook | Anything longer, such as crawls and large scrapes | The task is suspended and doesn't count toward compute usage. |
| Use Apify as an agent tool | AI agents that pick and run Actors themselves | The agent waits on a webhook task, so it doesn't count toward compute usage either. |
This task starts an Actor, waits for it to finish, and returns the items from the run's default dataset.
import { AbortTaskRunError, logger, task } from "@trigger.dev/sdk";
import { apify } from "./apify";
export const apifyActorRun = task({
id: "apify-actor-run",
maxDuration: 600,
retry: { maxAttempts: 3 },
run: async (payload: { actorId: string; input: Record<string, unknown> }) => {
const run = await apify.actor(payload.actorId).call(payload.input, {
timeout: 300, // Apify stops the Actor after 5 minutes, so it doesn't keep running after the task gives up
waitSecs: 360,
maxItems: 1000, // cap spend on pay-per-result Actors
});
if (run.status === "ABORTED") {
// Someone stopped the Actor on purpose, so don't start it again
throw new AbortTaskRunError(`Actor run ${run.id} was aborted`);
}
if (run.status !== "SUCCEEDED") {
throw new Error(`Actor run ${run.id} finished with status ${run.status}`);
}
const { items } = await apify.dataset(run.defaultDatasetId).listItems();
logger.info("Actor run finished", { runId: run.id, itemCount: items.length });
return { runId: run.id, datasetId: run.defaultDatasetId, items };
},
});timeoutstops the Actor on the Apify platform beforewaitSecsruns out, so the task never retries while an earlier Actor run is still going.maxItemscaps how many results a pay-per-result Actor charges you for. Background jobs run unattended, so a cap stops a runaway crawl.- A failed or timed-out Actor run throws an error, so the task's retry settings start the Actor again. An aborted run throws
AbortTaskRunErrorinstead, because someone stopped it on purpose. call()copies the Actor's log into your run logs. Passlog: nullin the options to turn this off.
To test the task, trigger it from the Trigger.dev dashboard with this payload. It runs Website Content Crawler on three pages:
{
"actorId": "apify/website-content-crawler",
"input": {
"startUrls": [{ "url": "https://docs.apify.com/academy" }],
"maxCrawlPages": 3,
"crawlerType": "cheerio",
"proxyConfiguration": { "useApifyProxy": true }
}
}For anything longer than a few seconds, start the Actor with start() instead of call() and attach an ad-hoc webhook that points at a Trigger.dev wait token URL. The task suspends on wait.forToken(), and Apify resumes it by calling the URL when the Actor run ends. A suspended task doesn't count toward Trigger.dev compute usage, however long the Actor runs.
The full task is in Trigger.dev's Apify Actor run guide. It relies on these Apify details:
- The webhook covers all four events that end an Actor run, so a failed, aborted, or timed-out run resumes the task with an error instead of leaving it waiting until the token times out.
- A
payloadTemplateof{ "eventType": {{eventType}}, "resource": {{resource}} }keeps the request small. Whatever Apify posts to the token URL becomes the token's result, so type the result after the template, not the default webhook payload. - The Actor's
timeoutis shorter than the token'stimeout, so the Actor always ends, and the webhook fires, before the token expires. - Webhook event types use underscores (
ACTOR.RUN.TIMED_OUT), but a run'sstatususes a hyphen (TIMED-OUT). Compare event types with event types and statuses with statuses.
Trigger.dev AI chat agents use AI SDK tools, so Apify can be a set of tools the agent calls. Trigger.dev's Apify tools example has the full code for three tools:
searchApifyActorssearches Apify Store, so the model picks an Actor at runtime instead of you hardcoding one. It returns monthly users and the pricing model, so the model can prefer widely used Actors.getApifyActorInputSchemagives the model the Actor's real input fields. Without it, the model guesses field names and Apify rejects the run.runApifyActorruns the Actor through the webhook task from the previous section, so the agent doesn't count toward compute usage while the Actor works. It returns at most 50 items and thedatasetId, because a dataset can be larger than the model's context window.
Set needsApproval: true on runApifyActor to pause the agent before it spends money on a run. Your frontend shows the pending call and sends the user's answer, as described in Trigger.dev's human-in-the-loop guide.
- Canceling a Trigger.dev run doesn't stop the Actor run it started. Abort the Actor run in Apify Console or with
apify.run(runId).abort(). - The tasks on this page and in the Trigger.dev guides aren't retry-safe: every retry starts a new Actor run. In production, store the Actor run ID and reuse that run on retry instead of starting a new one.
apify-client was bundled into the task. Add it to build.external in trigger.config.ts, as shown in Set up Apify in your Trigger.dev project, and run trigger dev or deploy again.
Some Actors need extra permissions before the API can run them. runApifyActor then returns Apify's error with a link to approve the permissions in Apify Console. Approve them there and ask the agent again, or let the model pick a different Actor.
For questions or help, reach out on the Apify developer community on Discord.