Runnel 2.8: replay any workflow from a failed step.Read the release

Skip to content
Runnel
Documentation

    Docs menu

    Defining jobs

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

    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.

    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:

    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.

    OptionTypeDefaultDescription
    retriesobject3 attempts, exponentialHow failed attempts are retried. See Retries and timeouts.
    timeoutduration'15m'How long one attempt may run before it is stopped and counted as failed.
    concurrencyobjectnone{ limit, key? }: how many runs of this job run at once, overall or per key.
    queuestring'default'Workers can listen to some queues and not others.
    descriptionstringnoneShown in the dashboard and returned by the 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:

    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.

    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.

    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}
    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:

    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.

    Edit this page