# Run a Steel browser job with Trigger.dev
URL: /cookbook/trigger-dev-browser-job

---
title: Run a Steel browser job with Trigger.dev
description: Queue a Trigger.dev task that creates a Steel session, drives Playwright over CDP, saves artifacts, and releases the browser in cleanup.
---

<RecipeJsonLd slug="trigger-dev-browser-job" title={"Run a Steel browser job with Trigger.dev"} description={"Queue a Trigger.dev task that creates a Steel session, drives Playwright over CDP, saves artifacts, and releases the browser in cleanup."} authors={[{"handle":"junhsss","name":"Jun Ryu"}]} datePublished="2026-06-29" dateModified="2026-06-29" sourceUrl="https://github.com/steel-dev/steel-cookbook/tree/e86cfbf8ba715cdbbc49fc2ef13e9fd7798695dc/examples/trigger-dev-browser-job" />

<RecipeMeta href="https://github.com/steel-dev/steel-cookbook/tree/e86cfbf8ba715cdbbc49fc2ef13e9fd7798695dc/examples/trigger-dev-browser-job" path="examples/trigger-dev-browser-job" authors={[{"handle":"junhsss","name":"Jun Ryu","avatar":"https://github.com/junhsss.png?size=40"}]} updated="2026-06-29" />

<RecipeQuickstart slug="trigger-dev-browser-job" />

This recipe runs browser automation as a queued background job. The request
path only enqueues `steel-browser-job`; the task creates a Steel session,
connects Playwright over CDP, extracts a page summary, saves artifacts, and
releases the session in `finally`.

The core workflow lives in `src/trigger/browser-job.ts`:

```ts
export const browserJob = task({
  id: "steel-browser-job",
  maxDuration: 300,
  retry: { maxAttempts: 3 },
  queue: { concurrencyLimit: 2 },
  run: async (payload) => {
    session = await steel.sessions.create({ sessionTimeout: 600000 });
    browser = await chromium.connectOverCDP(
      `${session.websocketUrl}&apiKey=${steelApiKey}`
    );
    // browser work
  },
});
```

`maxDuration` caps runaway jobs at 5 minutes. `retry` gives transient page or
network failures another attempt. `queue.concurrencyLimit` keeps only two
browser jobs active at once, so a burst of requests does not create an
unbounded number of sessions.

## Run it

```bash
cd examples/trigger-dev-browser-job
cp .env.example .env
npm install
npm run dev
```

Set `STEEL_API_KEY`, `TRIGGER_SECRET_KEY`, and `TRIGGER_PROJECT_REF` in `.env`.
Get a Steel key at [app.steel.dev/settings/api-keys](https://app.steel.dev/settings/api-keys).
Use your Trigger.dev project ref from the Trigger.dev dashboard.

In another terminal, enqueue one run:

```bash
npm run trigger
```

The trigger script reads `TARGET_URL` and `LINK_LIMIT` from `.env`, calls
`tasks.trigger("steel-browser-job", payload)`, and prints the run id. Watch the
run in the Trigger.dev dashboard. Task output includes the Steel Live View URL,
a hosted screenshot URL, local artifact paths, the extracted links, and
duration in milliseconds.

Local artifacts are written to `ARTIFACT_DIR`:

```text
artifacts/
|-- browser-job-2026-06-29T10-30-00-000Z.png
`-- browser-job-2026-06-29T10-30-00-000Z.md
```

## Why the browser lives in the task

Browser sessions are slow compared to HTTP handlers. A page can take 20-60
seconds when the site hydrates, retries, or challenges automation. Putting that
work in a Trigger.dev task gives you a run record, logs, retries, a timeout,
and queue backpressure. The API caller gets a run id immediately instead of
waiting for the browser.

The task still releases the Steel session on every path:

```ts
finally {
  if (browser) await browser.close();
  if (session) await steel.sessions.release(session.id);
}
```

That cleanup is the cost control. If extraction throws after navigation, the
remote browser still shuts down instead of idling until the session timeout.

## Make it yours

- **Swap the extraction.** Replace the `page.evaluate` block with your site's
  selectors, form submission, or file download flow.
- **Store artifacts durably.** Keep the hosted screenshot URL for public pages,
  or upload the `page.screenshot()` bytes to your own object storage when the
  artifact depends on logged-in session state.
- **Tune concurrency.** Raise `queue.concurrencyLimit` for high-throughput
  crawls, or lower it when each job holds a logged-in profile.
- **Add idempotency.** Pass an idempotency key from the caller when the same
  URL should not create duplicate browser runs.

## Related

[Playwright recipe](/cookbook/playwright) |
[Files recipe](/cookbook/files) |
[Trigger.dev tasks](https://trigger.dev/docs/tasks/overview)

## Related recipes

<RecipeGrid>
<RecipeCard slug="temporal-browser-workflow" title={"Run a durable browser workflow with Temporal"} description={"Build a Temporal TypeScript Workflow that schedules retryable Steel browser Activities to capture page summaries, screenshots, and Markdown artifacts."} topics={['Browser automation', 'Temporal']} languages={['TypeScript', 'Python', 'Rust', 'Go']} date="2026-06-29" />
<RecipeCard slug="headless-chrome" title={"Automate a cloud browser with headless_chrome"} description={"Use Steel with headless_chrome, the synchronous Rust equivalent of Puppeteer, to connect over CDP and scrape quotes with element handles."} topics={['Browser automation']} languages={['Rust']} date="2026-06-24" />
<RecipeCard slug="chromedp" title={"Automate a cloud browser with chromedp"} description={"Use Steel with chromedp to connect over CDP, navigate to Hacker News, extract the top stories, and capture a screenshot."} topics={['Browser automation']} languages={['Go']} date="2026-06-23" />
</RecipeGrid>
