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

# Workflows Quick Start

> Define a workflow, run it locally, push and deploy it, start a run, approve the card and read the result

## What you will build

A `ticket-plan` workflow that loads a ticket, asks your agent for an implementation plan, pauses for your approval, and records the outcome. By the end you will have started a run from the CLI, approved it, and read its output.

<Note>
  Workflows are a per-agent feature. If `lua workflows list` reports that workflows are not found on your agent, ask your organisation admin to enable them.
</Note>

<Steps>
  <Step title="Create the workflow file">
    Inside a lua-cli project (`lua init` if you do not have one), create `src/workflows/ticket-plan.ts`:

    ```typescript theme={null}
    import { z } from 'zod';
    import { createStep, createWorkflow, fromStep, template } from 'lua-cli';

    const Ticket = z.object({ id: z.string(), title: z.string(), body: z.string() });

    const loadTicket = createStep({
      id: 'loadTicket',
      inputSchema: z.object({ ticketId: z.string() }),
      outputSchema: Ticket,
      timeoutSeconds: 60,
      async execute({ inputData, log }) {
        // Replace with your tracker's API. Anything you return must match outputSchema.
        log(`loading ${inputData.ticketId}`);
        return { id: inputData.ticketId, title: 'Add CSV export', body: 'Users want to export the orders table.' };
      },
    });

    const Plan = z.object({
      summary: z.string(),
      files: z.array(z.string()),
      estimate: z.enum(['S', 'M', 'L']),
    });

    const record = createStep({
      id: 'record',
      inputSchema: z.any(),
      outputSchema: z.object({ approved: z.boolean(), summary: z.string() }),
      timeoutSeconds: 60,
      async execute({ getStepResult }) {
        const decision = getStepResult<{ approved: boolean }>('approvePlan');
        const plan = getStepResult<z.infer<typeof Plan>>('plan');
        return { approved: decision.approved, summary: plan.summary };
      },
    });

    export const ticketPlan = createWorkflow({
      name: 'ticket-plan',
      description: 'Load a ticket, draft a plan, ask for approval, record the decision.',
      inputSchema: z.object({ ticketId: z.string() }),
      outputSchema: z.object({ approved: z.boolean(), summary: z.string() }),
      budget: { maxCredits: 20 },
    })
      .then(loadTicket)
      .agentStep('plan', {
        agentId: '$self',
        // Ticket text is external content, so the turn runs with no tools.
        toolScope: {},
        prompt: template(
          'Ticket ${stepResults.loadTicket.id}: ${stepResults.loadTicket.title}\n\n${stepResults.loadTicket.body}\n\nWrite a short implementation plan.'
        ),
        outputSchema: Plan,
      })
      .approval('approvePlan', {
        title: 'Start work on this ticket?',
        details: template('${stepResults.plan.summary} (estimate ${stepResults.plan.estimate})'),
        approver: 'creator',
        timeoutHours: 72,
        onTimeout: 'deny',
      })
      .then(record)
      .commit();
    ```

    Names must match `^[a-z][a-z0-9-_]*$`; step ids must match `^[a-z][a-zA-Z0-9_-]{0,63}$`. The compiler finds every `createWorkflow(...).commit()` chain in your project - you can also list them on your `LuaAgent` under `workflows: [ticketPlan]`.
  </Step>

  <Step title="Run it locally">
    `lua test workflow` compiles the project and drives the graph offline. Agent steps are faked by default and approvals can be pre-answered:

    ```bash theme={null}
    lua test workflow --name ticket-plan --input '{"ticketId":"TP-12"}' --approve approvePlan
    ```

    Give the faked agent step a realistic output with `--step-output`:

    ```bash theme={null}
    lua test workflow --name ticket-plan \
      --input '{"ticketId":"TP-12"}' \
      --step-output 'plan={"summary":"Add an export button","files":["src/orders.ts"],"estimate":"S"}' \
      --approve approvePlan
    ```

    `lua workflows run ticket-plan ...` is the same command under the workflows verb. Pass `--agents live` to call the real agent instead of the fake.
  </Step>

  <Step title="Push it">
    ```bash theme={null}
    lua push workflow
    ```

    Pushing mints a new immutable version of the workflow on the server and records it in `lua.skill.yaml` under `workflows:`. Nothing is live yet.

    <Note>
      `lua push all` does not stage workflows - push them explicitly with `lua push workflow` (aliases: `workflows`, `wf`). Add `--auto-deploy` to publish the pushed version straight away.
    </Note>
  </Step>

  <Step title="Deploy it">
    ```bash theme={null}
    lua workflows deploy ticket-plan -v latest
    ```

    Deploying makes that version the active one and, on an agent under versioning, records a new agent version (the `agentVersion` the command prints). `lua workflows view ticket-plan` shows the versions and which one is active.
  </Step>

  <Step title="Start a run and watch it">
    ```bash theme={null}
    lua workflows start ticket-plan --input '{"ticketId":"TP-12"}' --follow
    ```

    `start` answers immediately with a run id (`wfr_...`) and `--follow` attaches the live event stream. The stream prints one line per event and stops at the approval:

    ```
    Run wfr_3f2... · queued
    [10:14:02] run.started
    [10:14:02] step.started · loadTicket
    [10:14:03] step.completed · loadTicket
    [10:14:03] step.started · plan
    [10:14:11] step.completed · plan
    [10:14:11] run.suspended · approvePlan {"stepId":"approvePlan","kind":"approval"}
    run waits for a person (approval · approvePlan) — lua workflows approve wfr_3f2... --approval <id> --decision approve|deny; then: lua workflows watch wfr_3f2...
    ```

    The command exits with code `8` (parked on a person). Pass `--input @file.json` to read the input from a file.
  </Step>

  <Step title="Approve the card">
    <Tabs>
      <Tab title="Desktop">
        Open **Runs** in the desktop, pick the run, and answer the card in the **Needs you** section of the run page. The same card also appears in your Inbox.
      </Tab>

      <Tab title="CLI">
        Approvals are resolved by their approval id (`wfa_...`). Read it off the run:

        ```bash theme={null}
        lua workflows status wfr_3f2... --json | jq -r '.data.suspensions[].suspend.approvalId'
        ```

        Then decide:

        ```bash theme={null}
        lua workflows approve wfr_3f2... --approval wfa_9c1... --decision approve --note "Go ahead"
        ```

        `--decision deny` completes the approval as denied; the run continues and `record` sees `approved: false`.
      </Tab>
    </Tabs>
  </Step>

  <Step title="Read the result">
    Re-attach and wait for the terminal event:

    ```bash theme={null}
    lua workflows watch wfr_3f2...
    ```

    Then read the run and its steps:

    ```bash theme={null}
    lua workflows status wfr_3f2... --steps
    ```

    ```
    Run wfr_3f2... · completed
       Workflow: ... @ ... (a1b2c3d4e5f6)
       Trigger:  api
       Output:   {"approved":true,"summary":"Add an export button"}

    Step         Kind      Status     Attempt  Error
    loadTicket   code      completed  1
    plan         agent     completed  1
    approvePlan  approval  completed  1
    record       code      completed  1
    ```

    `lua workflows status <runId> --json` returns the full run document, including each step's input and output preview.
  </Step>
</Steps>

## Start a run from a skill

A tool can start the same workflow on the user's behalf. The idempotency key makes a retried tool call return the original run instead of a second one:

```typescript theme={null}
import { Workflows } from 'lua-cli';

const { runId, status } = await Workflows.start(
  'ticket-plan',
  { ticketId: input.ticketId },
  { idempotencyKey: `ticket-plan:${input.ticketId}`, correlationKey: input.ticketId, tags: ['triage'] }
);
```

See [Authoring](/workflows/authoring#starting-runs-from-code) for the full `Workflows` API.

## Where to go next

<CardGroup cols={2}>
  <Card title="Authoring" icon="code" href="/workflows/authoring">
    Every builder call, binding helper, retry and approval option
  </Card>

  <Card title="Job tier" icon="server" href="/workflows/job-tier">
    Give a step a repository checkout and a coding session
  </Card>

  <Card title="Runs and events" icon="timeline" href="/workflows/runs-and-events">
    Statuses, the ledger, SSE and the desktop Runs pages
  </Card>

  <Card title="CLI reference" icon="terminal" href="/cli/workflows-command">
    All `lua workflows` subcommands and flags
  </Card>
</CardGroup>
