# Defining jobs

> How to define a job with runnel.job: its name, payload type, options and the context object every run receives.

Source: https://galley.themes.dev/docs/guides/defining-jobs/

A job is a named function in your code that Runnel can queue, retry and record. You define it once, export it, and register it with a worker. The same definition is used to enqueue runs from anywhere in your application.

## A minimal job

Give the job a name and a `run` function. The name is the job's identity in the dashboard, the CLI and the API, so pick something short and stable: `send-invoice`, not `sendInvoiceV2Final`.

```ts title="src/jobs/send-invoice.ts"
import { runnel } from '@runnel/sdk';

type Payload = { invoiceId: string; accountId: string };

export const sendInvoice = runnel.job({
  name: 'send-invoice',
  run: async (payload: Payload, ctx) => {
    const invoice = await db.invoices.find(payload.invoiceId);
    ctx.log.info('rendering', { invoiceId: invoice.id, lines: invoice.lines.length });
    const pdf = await renderPdf(invoice);
    await mailer.send({ to: invoice.email, attachments: [pdf] });
    return { sentTo: invoice.email };
  },
});
```

Whatever `run` returns is stored as the run's `output` and shown in the dashboard. Keep it small: an id or a count, not the PDF.

> **Warning** Renaming a job creates a new job. Runs already queued under the old name stay with it, so deploy the new name alongside the old one until the old queue is empty.

## Payloads

The payload is serialised to JSON when you enqueue and parsed before `run` is called. Dates arrive as strings, class instances lose their methods, and anything over 256 KB is rejected. Pass ids and look the record up inside the job; the data is then fresh when the run starts, which matters for delayed runs and retries.

The payload type flows through to `enqueue`, so TypeScript checks every call site:

```ts
await sendInvoice.enqueue({ invoiceId: 'inv_8812', accountId: 'acc_42' });
await sendInvoice.enqueue({ invoiceId: 8812 }); // type error: number is not a string
```

## Options

Every option is optional. The defaults suit most jobs; set what you need.

| Option | Type | Default | Description |
|---|---|---|---|
| `retries` | object | 3 attempts, exponential | How failed attempts are retried. See [Retries and timeouts](/docs/guides/retries/). |
| `timeout` | duration | `'15m'` | How long one attempt may run before it is stopped and counted as failed. |
| `concurrency` | object | none | `{ limit, key? }`: how many runs of this job run at once, overall or per key. |
| `queue` | string | `'default'` | Workers can listen to some queues and not others. |
| `description` | string | none | Shown in the dashboard and returned by the [Jobs API](/docs/reference/jobs-api/). |

Concurrency keys are useful when one customer can flood a job. This limits each account to two runs at a time while other accounts carry on:

```ts
concurrency: { limit: 2, key: (p) => p.accountId },
```

## The run context

The second argument to `run` is the context. It is the same object in jobs and workflows.

- `ctx.run.id`, `ctx.run.attempt`, `ctx.run.maxAttempts`: where you are. Useful in logs and for idempotency checks.
- `ctx.log.info|warn|error(message, fields)`: structured logs, attached to the run and searchable since 2.5.
- `ctx.signal`: an `AbortSignal` that fires when the run is cancelled or times out. Pass it to `fetch` and database drivers.
- `ctx.step`, `ctx.sleep`, `ctx.waitForEvent`: only meaningful in workflows. See [Workflows](/docs/guides/workflows/).

Anything you `console.log` is captured too, but without fields or levels.

## Python and Go

The same job in the other SDKs. Names, options and defaults are identical.

```python title="jobs/send_invoice.py"
from runnel import Runnel, Retries

runnel = Runnel()

@runnel.job(name="send-invoice", retries=Retries(attempts=5, backoff="exponential"), timeout="2m")
async def send_invoice(payload: dict, ctx) -> dict:
    invoice = await db.invoices.find(payload["invoice_id"])
    ctx.log.info("rendering", invoice_id=invoice.id)
    await mailer.send(to=invoice.email, attachments=[await render_pdf(invoice)])
    return {"sent_to": invoice.email}
```

```go title="jobs/send_invoice.go"
var SendInvoice = runnel.NewJob("send-invoice",
	func(ctx runnel.Context, p Invoice) error {
		ctx.Log().Info("rendering", "invoice_id", p.ID)
		return mailer.Send(ctx, p.Email, renderPDF(p))
	},
	runnel.WithRetries(5, runnel.Exponential),
	runnel.WithTimeout(2*time.Minute),
)
```

## Registering the job

A job only runs once a worker knows about it. Add it to the worker's list and deploy:

```ts title="src/worker.ts"
import { runnel } from '@runnel/sdk';
import { sendInvoice } from './jobs/send-invoice';
import { usageReport } from './jobs/usage-report';

runnel.worker({ jobs: [sendInvoice, usageReport], concurrency: 50 }).start();
```

`runnel dev` picks up the change straight away. For staging and production, run `runnel deploy`, described in [Deploying workers](/docs/guides/deploying/).
