> ## Documentation Index
> Fetch the complete documentation index at: https://docs.marina.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Jobs

> Run durable background work with marina.jobs, enqueued from requests or on a schedule.

Jobs run outside the request cycle, pinned to the exact app version that
enqueued them. Declare the service and each job in `marina.json`; the
`handler` names a function in your app's `jobs` map.

```json marina.json theme={null}
{
  "schema": 1,
  "type": "dynamic",
  "entrypoint": "src/app.ts",
  "runtime": { "jobs": "v1" },
  "jobs": {
    "refresh-dashboard": {
      "handler": "refreshDashboard",
      "schedule": "0 4 * * *",
      "retries": 3,
      "timeoutSeconds": 60
    }
  }
}
```

```ts theme={null}
import { defineApp } from "@marina/runtime";

export default defineApp({
  async fetch(_request, marina) {
    const run = await marina.jobs.enqueue(
      "refresh-dashboard",
      { dashboardId: "daily" },
      { dedupeKey: "daily:2026-08-24" },
    );
    return Response.json(run, { status: 202 });
  },
  jobs: {
    async refreshDashboard(input, marina, context) {
      // context.runId is stable across retries; use it as the idempotency key.
      await marina.db.query("select refresh_dashboard($1, $2)", [
        context.runId,
        input.dashboardId,
      ]);
    },
  },
});
```

`marina.jobs.get(runId)` returns the run's state (`queued`, `running`,
`succeeded`, or `failed`) with attempt and timing metadata.

## Delivery

* Delivery is **at least once**: a handler can run more than once for the same
  work, so make its effects idempotent using `context.runId`.
* A `dedupeKey` makes an enqueue idempotent for the caller: repeating the same
  key returns the original run instead of creating another.
* Handlers retry up to the declared `retries` on failure, within
  `timeoutSeconds` per attempt.

## Schedules

`schedule` is a five-field cron expression, evaluated in UTC. `*`, comma
lists, ranges, and `/` steps are supported. If the app was down over an
interval, at most one catch-up run fires, then the schedule advances.

Publishing a new version atomically replaces the previous version's
schedules. Runs already enqueued finish against the version that created
them.

## Limits

| Limit | Value |
| - | - |
| Jobs per app | 50 |
| `retries` | 0–9 |
| `timeoutSeconds` | 1–300 |
| Job input | JSON, up to 256 KiB, 32 levels deep |

## Local development

Under [`marina dev`](/guides/local-development), enqueued jobs run
immediately in-process so you can watch them work; schedules fire only with
`--schedules`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.