Skip to main content
After this guide, one step of your workflow 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.
  • A workflow that compiles (Author a workflow).
1

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.
src/workflows/repo-check.ts
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>.
2

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

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

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.
src/workflows/parallel-fix.ts
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.
5

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.
Output
Offline, git push uses your own credentials and skips the platform’s secret scan and branch admission, so point --workspace at a scratch repository.
6

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

Options you may need

Size classes

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

What a Job step costs

A Job-tier agent step is charged per model reply. A coding turn that thinks for an hour and answers two hundred times is charged two hundred times, not once — the attempt is not the unit, and neither is the step. This is the largest single thing to know before you give a coding turn a long timeout. What one reply costs depends on your plan: The price band comes from how much context the call carried and how many tools it fired. A cached prompt token counts toward that size at the fraction of a fresh token the provider charges Lua for it, and a cache write at its own fraction — so a long coding turn that re-reads the same cached repository context on every reply is sized by what the cache actually costs and lands in a lower band than its raw token count suggests, instead of being sized as though it re-read the whole repository each time. The band is what is charged: cached tokens move which band a call lands in, they are never a charge of their own, and a call whose weighted size stays inside the same band costs exactly what it cost before. Where Lua has no published fraction for a provider, nothing changes for that provider: its calls are sized exactly as they were before. The same rule applies to a chat turn, so a Job reply and a chat turn of the same shape land in the same band. The first message of a long conversation writes its context to the cache, so it usually lands in a higher band than the replies after it. Which model a Job step runs on is not the agent’s model. A Job step runs on the model the step pins; failing that, the platform’s default model for Job steps; failing that, your organization’s default. An agent whose own model is small does not make its Job steps cheap — they run on the platform default until the step pins something else, and the multiplier follows that model. Three places tell you which, before you are charged for it:
  • lua push workflow and lua workflows deploy return a job-model-default advisory for every Job step without a model, naming the model it will run on and that model’s multiplier.
  • The compose plan annotates each assigned Job step with the model and multiplier it will run on.
  • lua workflows status --steps prints the model, how it was chosen, and its multiplier for each Job step that has claimed.
Pin the model on the step when you want a cheaper one:
Before a Job step is dispatched, the platform prices one heavy call on the model that step will run on and checks it against what is left of the run budget. A step that cannot afford even that parks on the run budget before anything is spawned, so you raise the budget instead of paying for a container that stops on its first reply. While the attempt runs, its spend is checked against the cap on every heartbeat, and a crossing stops the turn at the next tool boundary rather than mid-reply. A Job step that makes no model call is not charged per reply, because there are no replies: a tier: 'job' code step, and the merge step that folds parallel worktree arms back together. On the legacy plan such a step reserves a flat 4 credits against the run budget when it claims, and that reserve is what it settles. On a seat plan it settles nothing — compute time on the Job tier is not priced yet. That difference is a known gap rather than a discount, and it is the one place the two plans disagree about the same step.

If it isn’t working

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.
A mount: 'ro' step ran before any rw step populated the volume. Put an rw step first, or mount rw.
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.

Next steps

Declare connections

How credentialsRef resolves to a connection on the agent that runs the workflow.

Operate runs

Parks, gates, retry-step, and the run page.

Workflow builder reference

Every Job-tier option on createStep and agentStep.

Stage agent changes

Load the lua-agent-builder plugin so a coding step stages changes to its own agent.