---
title: Your first durable job
description: Read the commerce Orders export task, its public job, provider binding, and accepted run handle.
pagination: false
sidebar:
  icon: rocket
---

Define an Orders export task, expose it through a job, and follow the run to completion. It is an optional extension after
the [Orders API tutorial](/docs/learn/build-and-run), not a file replacement in your protected
`relkit-orders` app. You need Bun, repository dependencies, and Docker for a live provider run.

## Pre-setup

Use a native task provider in local Docker. Inngest is the starting path here;
[Trigger.dev](/docs/modules/jobs/providers/trigger) and [Effect MQ](/docs/modules/jobs/providers/effect-mq)
also have Docker recipes. Choose one and review its [capabilities](/docs/modules/jobs/providers/comparison).

1. **Install Inngest and local tooling**

    {/* Generated by apps/docs/scripts/generate.ts. Do not edit. */}

    From this guide's application/package root, install only its selected requirements:

    ```sh
    bun add @relkit/docker@0.7.12 @relkit/inngest@0.8.1 @relkit/local@0.7.12 inngest@4.20.0
    ```

    Keep these owner-specific versions and the lockfile together. Runtime SDKs belong in `dependencies`; build/type tooling belongs in `devDependencies`. Installing an adapter does not supply its optional SDK peers or enable the feature's configuration.

2. **Configure the jobs profile**

    Merge `jobs` and `defaults.jobs` into your root configuration. Keep your existing ID,
    `env` import and other settings; the example imports the environment contract from `src/platform/env.ts`.

    ```ts title="relkit.config.ts"
    /** Declares task application defaults with inspection disabled until selected. */
    import { defineApp } from "@relkit/app/config";
    import env from "@app/platform/env.js";
    import { docker } from "@relkit/docker";
    import { inngest } from "@relkit/inngest";

    export default defineApp({
      id: "relkit-inngest",
      env,
      server: { port: 3000, maxBodyBytes: 1_048_576 },
      telemetry: { redaction: { mode: "development-redacted", maxBytes: 65_536 } },
      jobs: { default: docker(inngest()) },
      defaults: { jobs: "default" },
    });
    ```

    Docker supplies the local provider's connection values; there are no new `defineEnv` secrets
    for this recipe. Jobs with `profile: "default"` use this binding. `localJob()` is the older
    function-target provider and is not a substitute for the native task provider used here.

3. **Check and start after registering the task and job**

    Check Docker before continuing. After adding the resource and its service registration, run from the app root:

    ```sh title="Terminal — application root"
    docker version
    bun run check
    bun run typecheck
    bun run dev
    ```

    `check` validates the graph; it starts no containers. `dev` starts only Docker recipes used by
    that graph, waits for readiness, and binds their generated connection values. An unused profile
    alone does not start a container. Keep the server running and use `bun run relkit local status`
    in another terminal to inspect the services. [Local Docker](/docs/modules/integrations/local-docker)
    covers detached startup and stopping services while retaining their volumes.

    Use `bun run relkit jobs capabilities` to inspect the selected provider, then follow
    [Your first durable job](/docs/modules/jobs/quickstart) to submit and observe a run.

1. ## 1. Define a task

    When adapting this to a separate lean app, first complete [Inngest's selected installation and
    binding](/docs/modules/jobs/providers/inngest). The repository example already declares its native SDK and
    local tooling; copying task/job files does not install them. Provider creation tuples remain unavailable.

    In your own application, with an existing `orders` service and no export task yet, run:

    ```sh title="Terminal — application root"
    bun run relkit add task export-orders --service orders
    ```

    This creates `src/services/orders/tasks/export-orders.task.ts` and registers `exportOrdersTask` in
    the service's task map. Replace its placeholder input, output, and handler with your operation.
    The [task command reference](/docs/api/cli#relkit-add-task-name) lists execution and version options.
    Skip scaffolding when reading the commerce example, which already contains these files.

    A task owns durable execution. This one accepts order IDs, emits progress, and returns CSV text.
    Its helper `exportOrderCsv` lives in the commerce example; if adapting the pattern to your app,
    replace that helper with your own business operation and keep its schemas explicit.

    ```ts title="examples/commerce/src/services/orders/tasks/export-orders.task.ts"
    /** Produces demonstration CSV through the ordinary order helper and emits validated progress around the durable export. */
    import { defineTask } from "@relkit/app/tasks";
    import { z } from "@relkit/app/schema";
    import { exportOrderCsv } from "../helpers/fake-business.js";

    const exportOrders = defineTask({
      id: "orders.export-orders",
      version: "1",
      execution: "durable",
      input: z.object({ orderIds: z.array(z.string()) }),
      output: z.object({ csv: z.string(), exported: z.number().int().nonnegative() }),
      progress: z.object({
        completed: z.number().int().nonnegative(),
        total: z.number().int().positive(),
      }),
      handler: async ({ orderIds }, context) => {
        await context.progress.emit({ completed: 0, total: orderIds.length });
        const csv = exportOrderCsv(orderIds);
        await context.progress.emit({ completed: orderIds.length, total: orderIds.length });
        return { csv, exported: orderIds.length };
      },
    });

    export default exportOrders;
    ```

    The task ID and version are stable compatibility identifiers. Change the version when the input or output contract is intentionally incompatible.

2. ## 2. Expose a job

    In your own app, scaffold the job after creating the task:

    ```sh title="Terminal — application root"
    bun run relkit add job export-orders --service orders \
      --target orders.export-orders-task
    ```

    `--target` is the task's stable ID. This creates `src/services/orders/jobs/export-orders.job.ts`
    and registers the job on the service. Review its provider profile and client policy before
    running it. It does not create the HTTP trigger below. See the
    [job command reference](/docs/api/cli#relkit-add-job-name) for all flags.

    The job gives the task a public name and client policy. The HTTP route below is a separate
    entry point; its handler accepts a run and returns `202`, not the CSV output.

    ```ts title="examples/commerce/src/services/orders/jobs/export-orders.job.ts"
    /** Registers the export task as a public job exposing trigger, get, list and watch with progress/output fields. */
    import { defineJob } from "@relkit/app/jobs";
    import exportOrders from "../tasks/export-orders.task.js";

    const exportOrdersJob = defineJob({
      name: "exportOrders",
      id: "orders.export-orders",
      task: exportOrders,
      client: {
        public: true,
        operations: ["trigger", "get", "list", "watch"],
        fields: ["status", "progress", "output"],
      },
    });

    export default exportOrdersJob;
    ```

    ### Use this job from the frontend

    After [frontend setup and pulling the commerce contract](/guides/frontend/connect-react), render
    this component under the provider. It selects this job's public `exportOrders` name:

    ```tsx title="src/ExportOrders.tsx"
    "use client";

    /** Submits one commerce export and observes the accepted run without replaying an uncertain submission. */
    import { useJobRun, useJobTrigger, useRelkitClient } from "@relkit/client/react";

    /** Keeps the accepted receipt separate from the task's eventual output.
     * @returns One explicit submission button and its receipt, observation, or failure state. */
    export function ExportOrders() {
      const { status } = useRelkitClient();
      const submit = useJobTrigger("exportOrders");
      const run = useJobRun("exportOrders", submit.data ? { runId: submit.data.runId } : {});

      return (
        <section>
          <button
            disabled={status !== "ready" || !submit.isIdle}
            onClick={() => submit.mutate({ input: { orderIds: ["order-1"] } })}
          >
            Export orders
          </button>
          {submit.isPending && <p role="status">Submitting…</p>}
          {submit.isError && (
            <p role="alert">{String(submit.error)}. Check the outcome before submitting again.</p>
          )}
          {submit.data && <p>Run ID: {submit.data.runId}</p>}
          <p role="status">
            Connection: {run.connection}; run: {run.run?.status ?? "not accepted"}
          </p>
        </section>
      );
    }
    ```

    The task determines the `orderIds` input. The job's `client.operations` permits triggering and
    observation, while `client.fields` selects public run data. The returned `runId` is an admission
    receipt; completion and output arrive through the run observer. The example keeps uncertain
    submissions visible instead of automatically sending them again.

    Reference: [`useJobTrigger`](/docs/api/client#usejobtrigger), [`useJobRun`](/docs/api/client#usejobrun).
    Continue with [job clients and recovery](/docs/modules/jobs/clients).

    ### Add a separate HTTP trigger

    ```ts title="examples/commerce/src/routes/orders/export/route.ts"
    /**
     * Accepts an order export through the public orders service and returns its durable
     * run receipt. The service owns the job descriptor; this route owns HTTP transport.
     */
    import { defineRoute } from "@relkit/app/routes";
    import orders from "../../../services/orders/service.js";

    export const POST = defineRoute({
      id: "orders.export",
      handler: async (request) => {
        const input = (await request.json()) as { readonly orderIds: string[] };
        const run = await orders.exportOrders.trigger(input);
        return Response.json(run, { status: 202 });
      },
    });
    ```

    The commerce Orders service exports both the task and job. Its `relkit.config.ts` binds
    `jobs.default` to `docker(inngest())`, so the compiler can check durable capabilities. When
    adapting this to your own app, add a supported job binding before starting a run.

    The generated job owns admission and client operations while
    the task remains the unit of execution and compatibility.

    For the browser side of this declaration, follow the
    [React trigger and run example](/docs/modules/jobs/clients#use-from-react). It uses this job's
    `exportOrders` name and the task's generated input/output types.

3. ## 3. Trigger and observe

    The generated trigger receives `{ input, options }` and returns an accepted `RunHandle`. For example,
    call `client.jobs.exportOrders.trigger({ input: { orderIds }, options: { operationId } })`, then read
    `client.jobs.exportOrders.runs.get({ runId: accepted.runId })`. Keep the accepted `runId` and
    `operationId`; a handle is an admission receipt, not the task output.

    Use [job clients](/docs/modules/jobs/clients) for the complete trigger/watch lifecycle and
    [progress and streams](/docs/modules/jobs/progress-and-streams) for live updates.

4. ## 4. Run locally

    ```sh title="Terminal — examples/commerce"
    bun run check
    bun run typecheck
    bun ../../packages/cli/dist/bin.js local up --detach
    bun ../../packages/cli/dist/bin.js local status
    ```

    Start `bun run dev` after the provider reports ready. In another terminal, submit a run:

    ```sh title="Second terminal — examples/commerce"
    bun ../../packages/cli/dist/bin.js jobs capabilities
    curl -i -X POST http://127.0.0.1:4000/orders/export \
      -H 'content-type: application/json' \
      -d '{"orderIds":["order-1"]}'
    ```

    Expect HTTP `202` and a `RunHandle` with a `runId`; read or watch that ID until completion.
    The local provider is
    selected by the project binding. Use `relkit local reset --dry-run` before clearing local state.

    Run `bun run test` from `examples/commerce` before changing the task. A passing test checks
    application behavior, while the Docker run checks the selected provider.

{/* Generated by apps/docs/scripts/generate.ts. Do not edit. */}

**Reference and source**

- [`@relkit/app` API](/docs/api/app)
- [`@relkit/jobs` API](/docs/api/jobs)
- [`@relkit/schema` API](/docs/api/schema)
- [`@relkit/client` API](/docs/api/client)

- [`examples/commerce/src/services/orders/tasks/export-orders.task.ts`](https://github.com/rel-kit/relkit/blob/main/examples/commerce/src/services/orders/tasks/export-orders.task.ts)
- [`examples/commerce/src/services/orders/jobs/export-orders.job.ts`](https://github.com/rel-kit/relkit/blob/main/examples/commerce/src/services/orders/jobs/export-orders.job.ts)
- [`examples/commerce/src/routes/orders/export/route.ts`](https://github.com/rel-kit/relkit/blob/main/examples/commerce/src/routes/orders/export/route.ts)
