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

# Use the Job tier

> Run a step for hours in its own container with a git checkout, shell out to git and npm, and hand a coding turn to the agent

After this guide, one step of your [workflow](/concepts/workflows) runs in its own container against a clone of your repository, runs `git` and `npm` there, and an agent step edits files on the same checkout. Use the Job tier for a step that needs a filesystem, more than 600 seconds, or a coding turn; every other step stays on the worker tier, in the agent's sandbox.

*Verified against lua-cli 3.33.0.*

**Before you begin**

* A GitHub integration connected to the agent (`lua integrations connect --integration github`) and declared on the workflow by key; see [Declare connections](/build/workflows/connections).
* A workflow that compiles ([Author a workflow](/build/workflows/authoring)).

<Steps>
  <Step title="Declare the workspace and the steps">
    `workspace` on `createWorkflow` is the volume Job-tier steps mount: `kind: 'git'` clones a repository, `kind: 'empty'` is a blank volume. A step joins the tier with `tier: 'job'`, which a `workspace` mount implies.

    ```ts src/workflows/repo-check.ts expandable theme={null}
    import { z } from 'zod';
    import { createStep, createWorkflow, template } from 'lua-cli';

    const runTests = createStep({
      id: 'runTests',
      inputSchema: z.object({ repo: z.string(), ref: z.string() }),
      outputSchema: z.object({ head: z.string(), passed: z.boolean(), summary: z.string() }),
      tier: 'job',
      workspace: { mount: 'rw' },
      jobResources: 'medium',
      timeoutSeconds: 3600,
      async execute({ $, log }) {
        if (!$) throw new Error('runTests needs the Job tier');
        const head = await $.strict`git rev-parse --short HEAD`;
        const tests = await $`npm test`;
        log(tests.stdout.slice(-4000));
        return { head: head.stdout.trim(), passed: tests.code === 0, summary: tests.stdout.slice(-2000) };
      },
    });

    const Fix = z.object({ summary: z.string(), changedFiles: z.array(z.string()) });

    export const repoCheck = createWorkflow({
      name: 'repo-check',
      description: 'Clone a repository at a ref, run its tests, and have the agent fix failures.',
      inputSchema: z.object({ repo: z.string(), ref: z.string() }),
      outputSchema: Fix,
      budget: { maxCredits: 20, maxDurationSeconds: 8 * 3600 },
      connections: [{ key: 'github', integrationType: 'github', required: true }],
      workspace: {
        kind: 'git',
        repo: template('${initData.repo}'),
        ref: template('${initData.ref}'),
        credentialsRef: 'github',
        sizeGb: 10,
        ttlHours: 24,
      },
    })
      .then(runTests)
      .agentStep('fixFailures', {
        agentId: '$self',
        tier: 'job',
        harness: 'claude-code',
        workspace: { mount: 'rw' },
        jobResources: 'medium',
        timeoutSeconds: 7200,
        maxTurns: 200,
        retry: { maxAttempts: 2 },
        toolScope: { jobTools: ['shell', 'read', 'write', 'edit', 'glob', 'grep', 'git'] },
        prompt: template('The test run at ${stepResults.runTests.head} ended with: ${stepResults.runTests.summary}. Fix the failures and commit.'),
        outputSchema: Fix,
      })
      .commit();
    ```

    `repo` is `<host>/<owner>/<repo>`; a bare `owner/repo` means github.com and a full URL is normalized. `repo` and `ref` may each bind one whole run-input field with `template('${initData.repo}')`, never a composite. `credentialsRef` names the connection key that mints the checkout token. `sizeGb` and `ttlHours` (how long the volume outlives the run) default to 10 and 72. The clone happens on the first `rw` mount, and each run works on its own branch, `lua/wf-<lineageId>`.
  </Step>

  <Step title="Shell out from a code step">
    `mount: 'rw'` lets the step write; `'ro'` reads what an earlier `rw` step left. Inside `execute`, `ctx.exec(argv)` and the tagged template `` ctx.$`…` `` run one of `git`, `gh`, `pnpm`, `npm`, `npx`, `node`, `yarn`, `python3`, `pytest`, or `make` in the checkout. Both are typed optional because worker-tier steps lack them; guard before use as `runTests` does.

    There is no shell: literal text splits on whitespace, every `${value}` is exactly one argument, `&&`, pipes, and globs are refused, and a `cwd` option must stay inside the workspace. The result is `{ argv, code, signal, stdout, stderr, durationMs, truncated, timedOut }` with up to 1 MiB per stream; a non-zero exit is data in `code`, and `.strict` throws instead. `child_process` is refused at compile with `node-capability-unavailable`. On this tier `retry` re-arms an attempt that hit its time limit (`WALL_TIMEOUT`); an error your code throws is final, because re-running the same bundle would throw again.
  </Step>

  <Step title="Give the agent a coding turn">
    An agent step with `tier: 'job'` and a workspace is a coding turn on the checkout, driven by `harness: 'claude-code'` (or `'generic'`). `toolScope.jobTools` picks from `shell`, `read`, `write`, `edit`, `glob`, `grep`, `git`, `gh`, and `fetch`; unset, the turn gets the first seven. A `'ro'` mount drops `write`, `edit`, `git`, and `shell`, and `lua compile` warns `ro-step-has-no-tools` when nothing is left.

    A final reply that misses `outputSchema` fails the attempt, so pair it with `retry`; the workspace persists across attempts. Above 14,400 seconds the step must mount a workspace (`long-job-requires-workspace`), because a turn is split into four-hour segments that commit, push, and checkpoint at each boundary. `maxTurns` bounds one harness query; `maxMessages` and `maxInputTokens` bound the whole attempt.
  </Step>

  <Step title="Run arms in parallel worktrees">
    Several coding turns can work on one checkout at once. Give each arm `isolation: 'worktree'`, which gets its own branch `lua/wf-<lineageId>/<armId>`, and the `parallel` a `merge` policy that folds them back.

    ```ts src/workflows/parallel-fix.ts theme={null}
    import { z } from 'zod';
    import { createWorkflow, template } from 'lua-cli';

    const turn = {
      agentId: '$self',
      tier: 'job' as const,
      harness: 'claude-code' as const,
      workspace: { mount: 'rw' as const, isolation: 'worktree' as const },
      timeoutSeconds: 3600,
      retry: { maxAttempts: 2 },
    };

    export const parallelFix = createWorkflow({
      name: 'parallel-fix',
      inputSchema: z.object({ repo: z.string(), ticket: z.string() }),
      connections: [{ key: 'github', integrationType: 'github' }],
      workspace: { kind: 'git', repo: template('${initData.repo}'), ref: 'main', credentialsRef: 'github' },
    })
      .parallel(['implement', 'writeTests'], { merge: { strategy: 'rebase', onConflict: 'agent' } })
      .agentStep('implement', { ...turn, prompt: template('Implement ticket ${initData.ticket}.') })
      .agentStep('writeTests', { ...turn, prompt: template('Write tests for ticket ${initData.ticket}.') })
      .commit();
    ```

    `strategy` is `'rebase'` or `'merge'`; `onConflict: 'agent'` runs one resolver turn, `'fail'` fails the run. Worktree arms need a `merge` policy and a `merge` policy needs worktree arms; at most 8 arms may be worktrees.
  </Step>

  <Step title="Test it offline with --workspace">
    Offline, a Job-tier code step gets `ctx.workspace`, `ctx.exec`, and `ctx.$` against a directory you name, with the same allowlist on your own `PATH`; the agent step is faked and `--job-wall` splits it into virtual segments.

    ```bash theme={null}
    lua test --ci workflow --name repo-check --input '{"repo":"github.com/acme/app","ref":"main"}' --workspace ../app --job-wall 7200
    ```

    ```text Output theme={null}
    🧭 Running workflow locally: repo-check
    [18:04:13] run local-1789236253260 · 2 planned step(s)
    [18:04:13] runTests · exec · git rev-parse --short HEAD · exit 0 · 17 ms
    [18:04:13] runTests · exec · npm test · exit 0 · 118 ms
    [18:04:13] runTests · log ·
    > test
    > node -e "process.exit(0)"

    [18:04:13] runTests · completed
    [20:04:13] fixFailures · checkpointed · segment 1/1
    [20:04:13] fixFailures · completed
    [20:04:13] run · completed

    Workflow returned: Object — fields: summary, changedFiles
    Output:
    { summary: '', changedFiles: [] }
    …
    ```

    Offline, `git push` uses your own credentials and skips the platform's secret scan and branch admission, so point `--workspace` at a scratch repository.
  </Step>

  <Step title="Verify on the platform">
    After `lua push workflow --name repo-check` and `lua workflows deploy repo-check -v latest`, start a run and inspect its Job-tier steps.

    ```bash theme={null}
    lua workflows jobs <runId>
    lua workflows job-logs <runId> fixFailures --tail 500 --follow
    lua workflows workspace <runId>
    ```

    `jobs` lists each Job-tier step with status, size class, pod phase, segment, and heartbeat; `job-logs` polls the container log every 5 seconds until the step ends; `workspace` shows the repository, branch, head, and size, and `--release` frees the volume early.
  </Step>
</Steps>

## Options you may need

### Size classes

| `jobResources`    | CPU request / limit | Memory | Ephemeral storage |
| ----------------- | ------------------- | ------ | ----------------- |
| `small` (default) | 1 / 2               | 2 GiB  | 4 GiB             |
| `medium`          | 2 / 4               | 4 GiB  | 8 GiB             |
| `large`           | 4 / 8               | 8 GiB  | 16 GiB            |

A container over its memory limit fails the step with `job_oom_killed`; pick a larger class or bound the test suite (`--maxWorkers=2`).

### What the container may push

The container never holds a credential: the git remote is a local proxy that adds a short-lived token scoped to the pinned repository. A push is admitted only to the run's own branch or the arm's branch; `--force`, `--force-with-lease`, `--force-if-includes`, `--delete`, `--mirror`, `--prune`, `--all`, and `--tags` are refused, and a diff that contains a secret is refused before the push with `secret_in_diff`. `gh`, when granted, is limited to three writes on that repository: create a pull request, edit its `title`, `body`, or `draft` state, and comment.

### Limits

| Limit                                       | Value                                                                                                                  |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Code or tool step timeout                   | 1 to 14,400 s, default 3,600                                                                                           |
| Agent step timeout                          | 1 to 86,400 s, default 3,600; above 14,400 s a workspace is required                                                   |
| Segment length                              | 14,400 s, then checkpoint and re-spawn on the same attempt                                                             |
| `maxTurns`, `maxMessages`, `maxInputTokens` | 1 to 500; 1 to 5,000 (default 400); 1,000,000 to 500,000,000 (default 4,000,000)                                       |
| Worktree arms per `parallel`                | 8                                                                                                                      |
| Workspace volume                            | `sizeGb` default 10; kept `ttlHours` after the run, default 72; live workspaces per organization capped, 10 by default |
| Credits                                     | A Job-tier attempt settles 4 credits against `budget.maxCredits`                                                       |

## If it isn't working

<Accordion title="EXEC_UNAVAILABLE">
  The step called `ctx.exec` or `ctx.$` without a workspace mount, or offline without `--workspace <dir>`. Add `workspace: { mount: 'rw' }` to the step and a `workspace` to the workflow, or pass the directory.
</Accordion>

<Accordion title="workspace_not_ready">
  A `mount: 'ro'` step ran before any `rw` step populated the volume. Put an `rw` step first, or mount `rw`.
</Accordion>

<Accordion title="long-job-requires-workspace">
  An agent step over 14,400 seconds has no workspace to checkpoint to. Add `workspace: { mount: 'rw' }` and a `workspace` on `createWorkflow`, or shorten the step.
</Accordion>

## Next steps

<Columns cols={2}>
  <Card title="Declare connections" href="/build/workflows/connections">How `credentialsRef` resolves to a connection on the agent that runs the workflow.</Card>
  <Card title="Operate runs" href="/build/workflows/operate-runs">Parks, gates, `retry-step`, and the run page.</Card>
  <Card title="Workflow builder reference" href="/reference/sdk/workflow-builder">Every Job-tier option on `createStep` and `agentStep`.</Card>
</Columns>
