> ## 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.

# Job Tier

> Run a step for hours in its own container, with a git workspace and a coding session

## Overview

Worker-tier steps run in the agent's sandbox and are capped at 10 minutes. A step declared with `tier: 'job'` runs instead in its own Kubernetes Job: an isolated container with a mounted workspace volume, a checkpoint every few hours, and - for agent steps - a full coding session driven by a harness such as Claude Code.

Use the Job tier for anything that needs a repository checkout, a long test suite, or an agent that reads and edits files and runs commands.

```typescript theme={null}
const runTests = createStep({
  id: 'runTests',
  inputSchema: z.any(),
  outputSchema: z.object({ passed: z.boolean(), summary: z.string() }),
  tier: 'job',
  workspace: { mount: 'rw' },
  jobResources: 'large',
  timeoutSeconds: 3600,
  async execute({ workspace, log }) {
    const r = await $`cd ${workspace!.path} && npm ci && npm test -- --maxWorkers=2`.nothrow();
    log(r.stdout.slice(-4000));
    return { passed: r.exitCode === 0, summary: r.stdout.slice(-2000) };
  },
});
```

`$` is the Job image's shell helper; a plain `child_process` spawn also works in a Job-tier code step.

## Declaring a Job-tier step

| Option                             | Where                 | Notes                                                                                                                                                        |
| ---------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `tier: 'job'`                      | code step, agent step | Runs the step as a Job. Implied by `workspace`.                                                                                                              |
| `workspace: { mount, isolation? }` | code step, agent step | `mount: 'rw' \| 'ro'`; `isolation: 'shared'` (default) or `'worktree'`. The workflow must declare a `workspace`.                                             |
| `jobResources`                     | code step, agent step | `'small'` (default), `'medium'` or `'large'`.                                                                                                                |
| `timeoutSeconds`                   | code step, agent step | Code steps up to 14 400 s (4 h); agent steps up to 86 400 s (24 h). An agent step longer than 4 h must mount a workspace so it can checkpoint. Default 3600. |
| `jobTools`                         | code step             | Tools the code step may use, e.g. `['gh']` to mint a GitHub token for the step.                                                                              |
| `toolScope.jobTools`               | agent step            | The coding turn's tool allowlist (below).                                                                                                                    |
| `harness`                          | agent step            | `'claude-code'` (default) or `'generic'`.                                                                                                                    |
| `maxTurns`                         | agent step            | Coding-turn cap, 1..500. Absent means the platform default; a monorepo change usually needs more.                                                            |

## The workspace

Declare the volume once on `createWorkflow`:

```typescript theme={null}
export const delivery = createWorkflow({
  name: 'ticket-pilot-delivery',
  inputSchema: z.object({ repoUrl: z.string(), baseRef: z.string(), /* ... */ }),
  budget: { maxCredits: 120, maxDurationSeconds: 24 * 3600 },
  workspace: {
    kind: 'git',
    repo: template('input.repoUrl'),
    ref: template('input.baseRef'),
    credentialsRef: 'github',
    sizeGb: 10,
  },
})
```

| Field                | Notes                                                                                                                                   |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `kind`               | `'git'` (a clone) or `'empty'` (a blank volume).                                                                                        |
| `repo`, `ref`        | A literal, or `template('input.<field>')` resolved over the run input. The clone URL must be a `https://github.com/<owner>/<repo>` URL. |
| `credentialsRef`     | The id of the GitHub connection that mints the token. In an agent template it is the declared connection key.                           |
| `sizeGb`, `ttlHours` | Volume size and how long it is kept after the run.                                                                                      |
| `verify`             | A command recorded on the workspace (for example `npm test`).                                                                           |
| `keepArtefacts`      | Keep the volume's artefacts after release.                                                                                              |
| `backend`            | `'ebs'` today.                                                                                                                          |

### Mounting

* The first Job-tier step of a run must mount `rw` - the clone happens on the first rw mount, and an `ro` mount on an empty volume is refused.
* `mount: 'ro'` drops the `write`, `edit` and `git` tools from a coding turn.
* Each run works on its own branch, `lua/wf-<lineageId>`. When a turn completes (and at each checkpoint) the harness commits and pushes that branch; a diff that contains a secret is refused before the push (`secret_in_diff`).
* A child workflow started with `{ workspace: 'inherit' }` uses the parent's volume; the parent's volume is released while the child waits.
* `lua workflows workspace <runId>` shows the repo, branch, head, size and TTL; `--release` frees the volume early.

### Parallel worktree arms

Several coding turns can work on the same checkout at once. Each arm gets its own worktree and branch (`lua/wf-<lineageId>/<armId>`) and a merge row folds them back:

```typescript theme={null}
.parallel(['implement', 'writeTests', 'updateDocs'], { merge: { strategy: 'rebase', onConflict: 'agent' } })
.agentStep('implement', { agentId: 'swe-implementer', tier: 'job', workspace: { mount: 'rw', isolation: 'worktree' }, timeoutSeconds: 7200, prompt: template('...') })
.agentStep('writeTests', { agentId: 'swe-tester', tier: 'job', workspace: { mount: 'rw', isolation: 'worktree' }, timeoutSeconds: 5400, prompt: template('...') })
.agentStep('updateDocs', { agentId: 'swe-writer', tier: 'job', workspace: { mount: 'rw', isolation: 'worktree' }, timeoutSeconds: 1800, jobResources: 'small', prompt: template('...') })
```

`merge.strategy` is `'rebase'` or `'merge'`; `onConflict: 'agent'` lets one resolver turn fix conflicts, `'fail'` fails the merge. At most 8 arms.

## Coding turns

An agent step on the Job tier is a coding session on the mounted checkout:

```typescript theme={null}
const jobTurn = () => ({
  agentId: '$self',
  tier: 'job' as const,
  harness: 'claude-code' as const,
  workspace: { mount: 'rw' as const },
  jobResources: 'medium' as const,
  timeoutSeconds: 3600,
  maxTurns: 200,
  retry: { maxAttempts: 2 },
  toolScope: { jobTools: ['shell', 'read', 'write', 'edit', 'glob', 'grep', 'git'] },
});

createWorkflow({ /* ... */ })
  .then(admit)
  .agentStep('spec', {
    agentId: '$self', tier: 'job', harness: 'claude-code', workspace: { mount: 'rw' },
    jobResources: 'small', timeoutSeconds: 1800, maxTurns: 80,
    toolScope: { jobTools: ['read', 'glob', 'grep'] },      // read-only turn: no shell, no edit
    systemPrompt: ARCHITECT_ROLE.instructions,
    prompt: template(SPEC_PROMPT),
    outputSchema: Spec,
  })
  .agentStep('implement', { ...jobTurn(), prompt: template(IMPLEMENT_PROMPT), outputSchema: Result })
  .dowhile('reviewFix', and(eq(reviewRef.path('verdict'), lit('revise')), lt(reviewRef.path('round'), lit(2))), { maxIterations: 3 })
  .agentStep('reviewFix', { ...jobTurn(), prompt: template(REVIEW_FIX_PROMPT), outputSchema: Review })
  .then(openPr)
  .commit();
```

* **Harness.** `claude-code` runs the turn as a Claude Code session with the step's prompt, the agent's persona and the tool allowlist. `generic` is the other harness; it does not support the schema-repair pass a `claude-code` turn gets when its reply misses `outputSchema`.
* **Tool scope.** `toolScope.jobTools` picks from `shell`, `read`, `write`, `edit`, `glob`, `grep`, `git`, `gh` and `fetch`. Leave out `gh` when a code step opens the PR instead.
* **Output schema.** The turn's final reply is validated against `outputSchema`; a mismatch fails the attempt, so give the turn `retry: { maxAttempts: 2 }`. The workspace persists across attempts, so a retry continues rather than restarts.
* **Segments.** A long turn is split into 4-hour segments. At each boundary the harness commits, pushes and checkpoints; the next segment resumes from the checkpoint on the same attempt.
* **Model.** Leave `model` unset to use the organisation default for Job turns.

## 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 that exceeds its memory limit is killed and the step fails with `job_oom_killed`. Keep test suites inside the class (for example `--maxWorkers=2` on a memory-hungry suite) or pick a larger one.

## Credentials

The Job container never holds a credential.

* `workspace.credentialsRef` names a GitHub connection. At spawn the platform mints a **short-lived token scoped to that one repository** and the permissions the step needs, and hands it to a sidecar next to the container - not to the container itself.
* Inside the container the git remote is a local proxy. `git fetch`, `git pull` and `git push` work normally; the proxy adds the credential on the way out.
* A push is admitted only to the run's own branch (or the arm's branch): no other refs, no deletes, no force-push over a moved ref. A pod spawned without an exact ref refuses every push.
* `gh` (when granted in `jobTools` / `toolScope.jobTools`) goes through the same proxy and is limited to reading the pinned repository and three writes: create a pull request, edit a pull request's title/body/draft state, and comment on an issue or PR. Merging, other repositories and organisation-level calls are refused.
* Tokens are never written to a row, an event, a log or the journal. If the connection cannot mint a token the step fails with `credentials_revoked` (not retried).
* The model provider key is likewise held only by the sidecar; the container talks to the provider through a metered local proxy.

Steps that need other services use `requiredConnections` and the usual `Integrations.passthrough` from a worker-tier code step - the pattern in the shipped examples is to open the PR from a worker step under `ctx.once` after the coding turns finish.

## What the run page shows

On the desktop run page, select a Job-tier step to see:

* the Job's name, phase, size class and harness, with spawned / claimed / last-heartbeat times;
* elapsed time against the timeout, and the segment strip for a multi-segment turn;
* **What the agent is doing**: the turn's latest activity, newest first - every tool call with its input and, once it returns, whether it succeeded and its output; and the model's own notes between calls;
* the workspace panel: branch, head, size used and files changed.

The **Log** tab lists the same activity for the whole run with filters for **Tool calls**, **Agent notes** and **Step events**, and a **Follow** switch that keeps the newest line in view.

From the CLI:

```bash theme={null}
lua workflows jobs <runId>                              # every Job-tier step: status, size, pod phase, segment, heartbeat
lua workflows job-logs <runId> <stepId> --tail 500 --follow   # the container log, polled until the step ends
lua workflows workspace <runId>                          # the volume: repo, branch, head, size, arms
```

## Limits

| Limit                                    | Value                                                                                     |
| ---------------------------------------- | ----------------------------------------------------------------------------------------- |
| Code step timeout                        | 1..14 400 s (default 3600)                                                                |
| Agent step timeout                       | 1..86 400 s (default 3600); above 14 400 s a workspace is required                        |
| Segment length                           | 4 h, then checkpoint and re-spawn on the same attempt                                     |
| `maxTurns`                               | 1..500                                                                                    |
| Worktree arms per `parallel`             | 8                                                                                         |
| Job-tier concurrency and live workspaces | Capped per organisation; a step past the cap waits (`step.throttled`) rather than failing |
| Job seconds per run                      | A run budget dimension (`jobSeconds`); exhausting it parks the run on a `budget` gate     |
| Container memory                         | The size class limit (2 / 4 / 8 GiB); exceeding it fails the step with `job_oom_killed`   |

## Related

* [Authoring](/workflows/authoring) - every step and container option
* [Runs and events](/workflows/runs-and-events) - parks, gates, retry-step and the run page
* [Workflows Command](/cli/workflows-command#lua-workflows-jobs) - `jobs`, `job-logs` and `workspace`
