> ## Documentation Index
> Fetch the complete documentation index at: https://vlyai-1c28d863.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Background jobs

> Long-running work, retries, and durable workflows. The Convex action pattern for jobs that can't fit in a request.

Some work doesn't fit in a request: sending 10,000 emails, processing a video, periodic data sync. For these, use **Convex actions** with a job table and the scheduling primitives.

## The pattern

<Steps>
  <Step title="A job record lives in the schema">
    `jobs` table with status (`queued`, `running`, `completed`, `failed`), payload, attempts.
  </Step>

  <Step title="A mutation enqueues">
    Inserts a row in `jobs` with status `queued`, then schedules an action.
  </Step>

  <Step title="A scheduled action processes">
    Picks up `queued` jobs, runs them, updates status. Retries on failure with exponential backoff.
  </Step>

  <Step title="Clients observe via reactive query">
    `useQuery(api.jobs.byUser)` shows live status to the user.
  </Step>
</Steps>

## Example: bulk email send

```typescript convex/jobs.ts theme={null}
import { mutation, action } from "./_generated/server";
import { v } from "convex/values";
import { api } from "./_generated/api";

export const enqueueBulkEmail = mutation({
  args: { recipients: v.array(v.string()), subject: v.string(), html: v.string() },
  handler: async (ctx, args) => {
    const jobId = await ctx.db.insert("jobs", {
      type: "bulk_email",
      payload: args,
      status: "queued",
      attempts: 0,
    });
    await ctx.scheduler.runAfter(0, api.jobs.processEmailJob, { jobId });
    return jobId;
  },
});

export const processEmailJob = action({
  args: { jobId: v.id("jobs") },
  handler: async (ctx, { jobId }) => {
    const job = await ctx.runQuery(api.jobs.get, { id: jobId });
    if (!job || job.status !== "queued") return;
    await ctx.runMutation(api.jobs.markRunning, { id: jobId });

    try {
      for (const recipient of job.payload.recipients) {
        await fetch("https://api.resend.com/emails", { /* … */ });
      }
      await ctx.runMutation(api.jobs.markCompleted, { id: jobId });
    } catch (err) {
      const next = job.attempts + 1;
      if (next >= 3) {
        await ctx.runMutation(api.jobs.markFailed, { id: jobId, error: String(err) });
      } else {
        await ctx.runMutation(api.jobs.markRetry, { id: jobId, attempts: next });
        await ctx.scheduler.runAfter(60_000 * next, api.jobs.processEmailJob, { jobId });
      }
    }
  },
});
```

## Limits

* Action runtime cap: 10 minutes per invocation
* For longer work, split into chunks and reschedule each chunk

## Patterns to consider

<CardGroup cols={2}>
  <Card title="Inngest integration" icon="cog" href="/integrations/inngest">
    For complex multi-step workflows with steps, retries, and backoff built in.
  </Card>

  <Card title="Trigger.dev integration" icon="play">
    Job queue with a polished dashboard.
  </Card>

  <Card title="Scheduled functions" icon="clock" href="/convex/scheduled-functions">
    For recurring (cron-like) jobs.
  </Card>

  <Card title="Webhooks pattern" icon="webhook" href="/recipes/webhooks">
    For event-driven jobs.
  </Card>
</CardGroup>

## Related

<CardGroup cols={3}>
  <Card title="Convex actions" icon="zap" href="/convex/actions">
    The function type background jobs use.
  </Card>

  <Card title="Scheduled functions" icon="clock" href="/convex/scheduled-functions">
    Cron + delayed scheduling.
  </Card>

  <Card title="Email campaigns recipe" icon="megaphone" href="/recipes/email-campaigns">
    Worked example.
  </Card>
</CardGroup>


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