lead-outreach, a workflow that loads a lead, asks the model to draft an intro email, waits for you to approve the draft, and records the send. At the end, a run on the platform has paused for your decision, you have approved it from the terminal, and lua workflows status shows every step completed with the output {"sent":true}. Allow 20 minutes. You need a project created with lua init and a user session from lua auth configure (Install the CLI and sign in).
Verified against lua-cli 3.33.0.
What you’ll build
- A code step that loads a lead, and an agent step that drafts the email from that step’s output.
- An approval that shows you the draft, lets you edit its subject and body, and denies itself after 48 hours.
- A code step that records the send once, even when the platform retries it.
- An offline run with the approval answered from a flag, then a pushed and deployed version.
- A live run you follow to the approval, approve from the CLI, and read the result of.
1
Create the workflow file
Create Read it top to bottom.
src/workflows/lead-outreach.ts. Two createStep() objects hold your code; the createWorkflow() chain places them around an agent step and an approval and ends in .commit().src/workflows/lead-outreach.ts
loadLead returns a Lead; the platform validates that return value against outputSchema after every attempt. The agent step draftEmail runs one turn of your own agent ('$self') with no tools and must answer in the shape of Draft, because outputSchema is set. The template() string is rendered when the step is dispatched, so ${stepResults.loadLead.name} is the loaded lead’s name; keep it in single quotes so TypeScript never interpolates it. The approval shows the draft to whoever started the run and lets them edit subject and body; after 48 hours without a decision it counts as denied. recordSend receives the approval’s output, not the draft: approved is false on a denial or a timeout, and the edited draft arrives as editedPayload, else the step reads the original with getStepResult. sideEffects: 'external' stops the platform from re-running the step after a fault, onError: 'park' parks it for a person instead, and once() keeps the record from being written twice. budget caps the run at 10 credits (one per agent step) and three days. With the file saved, npx tsc --noEmit reports no errors.2
List the workflow on the agent
Only workflows listed on the Run A refused graph fails here with the file, the line, and a code such as
LuaAgent are compiled.src/index.ts
lua compile --ci and the summary counts the workflow:Output
closure-predicate; the compile also warns when a workflow with an approval has no budget.maxDurationSeconds, which is why the file sets one.3
Run it offline and approve the draft
lua test workflow drives the graph on your machine: your code steps run for real, the agent step returns a fake Draft shaped from its schema, and --approve answers the approval. --ci turns any prompt the driver would show into an error, so the command behaves the same in a script.Output
recordSend step wrote one entry to the outreach-log collection on your agent, which is the effect this workflow exists for. The loadLead · log line is your log() call. The agent step is faked here; --agents live sends it to your agent for a real draft.4
Run it again and deny
A denial is data, not an error: the run continues and Both paths of the graph now pass without a server. In a script, an approval you forgot to answer fails with exit 2 instead of waiting for a keyboard.
recordSend returns without writing.Output
5
Push a version
Pushing creates a version of the workflow on your agent and records it in The output names the version it created,
lua.skill.yaml under workflows:. Nothing runs it yet.1.0.0 for a first push; --set-version 1.1.0 chooses one, and lua workflows versions lead-outreach lists every version with a star on the active one. Workflows are not part of lua push all; push each one by name.6
Deploy it
Deploying makes the pushed version the one that runs.The command prints
✅ Version 1.0.0 of "lead-outreach" deployed and Workflow is live., and lua workflows list shows 1.0.0 in the Active column. Because the deploy also records an agent version scoped to this workflow, lua version promote <n> is your rollback; the full flow is in Release an agent to production.7
Start a run and follow it to the approval
start creates a run and returns its id; --follow streams its events.✅ Run <runId> · queued, then one line per event (run.started, step.completed · loadLead, step.completed · draftEmail) until the run parks on the approval. The stream ends with exit code 8 and the notice ⏸️ run waits for a person (approval · reviewDraft) — lua workflows approve <runId> --approval <id> --decision approve|deny; then: lua workflows watch <runId>. Copy the run id. Had nobody decided within --timeout, the command would have exited 7 with the run still parked; --idempotency-key <key> on start makes a repeated command return this run instead of starting another.8
Approve from the CLI
An approval is answered by its own id (The second command prints the outcome and the run’s new status,
wfa_…), never by the step id. Read it from the run, then decide.running; the note lands in the approval’s output as text, where recordSend could read it. To change the draft before sending, read it with lua workflows approval-payload <runId> --approval <approvalId> and pass your edit with --edit @draft.json --fingerprint <fingerprint>; see Add approvals and signals.9
Read the result
Re-attach and wait for the terminal event, then read the run with its steps.
watch prints step.completed · recordSend and run.completed, and exits 0. The status header shows Run <runId> · completed, Trigger: api, the budget spent (0 of 10 credits), and Output: {"sent":true}; the table that follows lists loadLead, draftEmail, reviewDraft, and recordSend, each completed on attempt 1. Add --json to read every step’s input and output preview as a document.lua workflows runs --workflow lead-outreach lists your run with Status completed and Trigger api.What you learned
- A workflow is a static graph:
createStepholds your code, the chain places steps, and.commit()validates it before anything runs. About workflows explains runs, steps, and containers. - Data flows by binding, not by closure:
template()placeholders and the previous step’s output are how a step sees upstream results. Author a workflow covers maps, predicates, and placement. - An approval’s output is the decision, not the payload, and a denial is data the next step reads. Add approvals and signals has every approver and timeout option.
- The offline driver runs your code for real and fakes only the model, so
--approveand--denycover both branches before you push. Test a workflow offline goes further. - A workflow is pushed and deployed on its own, and a run is operated by id:
start,watch,status,approve. Operate runs covers retries, budgets, and cancellation.
Next steps
Operate runs
Watch, repair, raise budgets, and cancel runs on the platform.
Workflow builder reference
Every chain method, option, and build error.
Use the Job tier
Give a step a repository checkout and a coding turn.

