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

# Choose a primitive

> Which of the eight ways to make an agent act fits your task, with a comparison table and a decision flowchart

A primitive is one way to make an agent do something, and there are eight: a [tool](/concepts/skills-and-tools) in a skill, a [webhook](/concepts/webhooks), a [trigger](/concepts/triggers), a [job](/concepts/jobs) (or a [dynamic job](/reference/sdk/jobs) created at runtime), a [workflow](/concepts/workflows), a [preprocessor or postprocessor](/concepts/processors), an [MCP server](/concepts/mcp-servers), and a runtime call from your own code ([`Agents.invoke`](/reference/sdk/agents) or [`Channels.send`](/reference/sdk/channels)). They differ in what starts them, whether a conversation is involved, how you test them, and how they go live. Picking the wrong one is the most common reason a task feels harder than it should: a webhook that chains three tools by hand wanted to be a workflow, and a job that loops over end users wanted to be a dynamic job per end user.

*Verified against lua-cli 3.33.0.*

## The eight ways compared

| Primitive                                     | Use it when                                                                          | Runs when                                                | Has a conversation?                                                           | Tested with                                                                  | Made live by                                                                                                                             | Retried by the platform                                                                              |
| --------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Tool in a skill (`LuaTool`, `LuaSkill`)       | An end user asks for something in a conversation                                     | The model calls it during a turn                         | Yes                                                                           | `lua test skill --name <tool>`, then `lua chat`                              | `lua push skill`, then `lua version create` and `lua version promote` (`lua deploy skill` is live at once but reset by the next promote) | No                                                                                                   |
| Webhook (`LuaWebhook`)                        | Another system sends HTTP and you own the code and the response                      | A request reaches its URL                                | No; it can call `Agents.invoke`                                               | `lua test webhook --name <name>`                                             | `lua push webhook`, then `lua deploy webhook`                                                                                            | Subscribed events: 3 attempts; direct requests: no ([webhooks](/concepts/webhooks))                  |
| Trigger (`defineTrigger`)                     | An event should become an agent turn, one tool call, or a workflow run               | A request reaches its paste-anywhere URL                 | Yes for a turn; no for `tool` or `startWorkflow`                              | No local runner: fire the URL, read `lua triggers logs`                      | `lua triggers create`, `lua push trigger`, then `lua deploy trigger`                                                                     | No; a slot failure answers the sender 500 so the sender retries                                      |
| Job (`LuaJob`) or dynamic job (`Jobs.create`) | Work on a schedule; a dynamic job is a one-off for one end user, created from a tool | `cron`, `interval`, or `once`                            | No; a dynamic job reaches its end user with `job.user()`                      | `lua test job --name <name>`; a dynamic job through the tool that creates it | `lua push job`, then `lua deploy job`; a dynamic job is created active                                                                   | Failed and timed-out attempts, per `retry` ([jobs](/concepts/jobs))                                  |
| Workflow (`createWorkflow`)                   | Several steps, an approval, fan-out, retries, or more than about ten minutes         | Started by code, the CLI, a trigger, a schedule, or chat | No; its agent steps and approvals are its own                                 | `lua test workflow --name <name>`                                            | `lua push workflow`, then `lua workflows deploy <name> -v latest`                                                                        | Per step: `retry` re-arms a failed or timed-out attempt; `onError` decides what a final failure does |
| Preprocessor, postprocessor                   | The same change to every message or every reply                                      | Before the model (pre) or after it (post), every turn    | Yes                                                                           | `lua test preprocessor`, `lua test postprocessor`                            | `lua push preprocessor`, then `lua deploy preprocessor`                                                                                  | No                                                                                                   |
| MCP server (`LuaMCPServer`)                   | Tools already served over MCP                                                        | The model calls one of its tools                         | Yes                                                                           | `lua chat` after push and activate; `lua logs --type mcp`                    | `lua push mcp`, then `lua mcp activate <name>`                                                                                           | A read-shaped tool once, on a timeout; never a write                                                 |
| `Agents.invoke`, `Channels.send`              | Your code needs another agent's full turn, or an outbound message                    | Your code calls it, inside any primitive                 | `Agents.invoke` opens a turn on the target; `Channels.send` sends without one | The primitive that calls it; `lua test` makes real calls                     | With that primitive                                                                                                                      | No; your code decides (a channel may retry its own delivery)                                         |

Three rules explain most of the table. `lua test` runs one primitive's `execute` on your machine with the input you give it and no model; only skills, webhooks, jobs, processors, and workflows have a test type, so a trigger or an MCP server is exercised on the live agent. A [sandbox](/concepts/environments) `lua chat` swaps in your local skills, processors, and persona, and uses whatever webhooks, jobs, triggers, workflows, and MCP servers are already live. And every code primitive except an MCP server is versioned: `lua push` uploads a version, `lua version create` snapshots the agent, and `lua version promote` makes the snapshot live for every primitive at once. `lua deploy <type>` is the single-primitive shortcut: it goes live at once for webhooks, jobs, preprocessors, postprocessors, and triggers by creating and promoting an agent version scoped to that primitive, and `lua workflows deploy` does the same for a workflow; `lua deploy skill --set-version <v>` serves that skill version from the agent's next turn on any agent, but the next `lua version promote` resets every skill to the promoted agent version's pin, so make a skill change durable with `lua version create` and `promote` ([releases and versions](/concepts/releases-and-versions)). The persona is not a primitive and is made live by push: a pushed persona is served from the agent's next message, and `lua deploy persona` switches it to an earlier version at once.

## A decision path

```mermaid theme={null}
flowchart TD
  A{"Several dependent steps, an approval, fan-out, or over ten minutes of work?"} -->|Yes| WF["Workflow"]
  A -->|No| B{"Started by an end user's message?"}
  B -->|Yes| C{"The same change to every message or reply?"}
  C -->|Yes| PP["Preprocessor or postprocessor"]
  C -->|No| T["Tool in a skill"]
  B -->|No| D{"Started by an HTTP call from another system?"}
  D -->|Yes| E{"Need to shape the HTTP response or run your own code?"}
  E -->|Yes| WH["Webhook"]
  E -->|No| TR["Trigger"]
  D -->|No| F{"Started by time?"}
  F -->|Yes| G{"One reminder for one end user, set from a tool?"}
  G -->|Yes| DJ["Dynamic job"]
  G -->|No| J["Job"]
  F -->|No| H{"Tools already served by an MCP server?"}
  H -->|Yes| MCP["MCP server"]
  H -->|No| I{"Called from your own code?"}
  I -->|"Another agent's full turn"| AI["Agents.invoke"]
  I -->|"A message outside the current reply"| CS["Channels.send"]
```

Two refinements. If the event comes from a SaaS you connected through Unified.to, subscribe to it with an [integration webhook](/integrations/events) and point it at a webhook or a trigger rather than building the intake yourself. If you only need one model call inside code, with no persona and no tools, call `AI.generate` instead of `Agents.invoke`; see [Compose agents](/build/compose-agents).

## Common combinations

* Trigger into a workflow with an approval. The vendor posts to the trigger URL, `transform` returns `{ startWorkflow }`, and the run pauses on `.approval()` until a person decides. Set `idempotencyKey` from the vendor's delivery ID; otherwise every redelivery starts another run.
* Webhook plus job. The webhook records each event in `Data` as it arrives; a nightly job aggregates the day and sends the summary with `Channels.send`.
* Tool plus dynamic job. The tool creates a job with `schedule: { type: 'once' }` and puts everything it needs in `metadata`, because `execute` is serialized and can't close over variables. When it fires, `job.user()` returns the end user who asked.
* Tool plus preprocessor for human handoff. The tool sets a flag on the end user; a preprocessor returns `{ action: 'block' }` while the flag is set and notifies a person with `Channels.send`. See [Add human handoff](/build/add-human-handoff).
* Webhook into `Agents.invoke`. When an HTTP event needs a model decision but the sender expects a specific response body, verify and answer in the webhook, then invoke the agent with the end user's `userId`.

## Next steps

<Columns cols={2}>
  <Card title="Add a tool" href="/build/add-a-tool">Write a tool, register its skill, and test it with `lua test skill`.</Card>
  <Card title="Handle a webhook" href="/build/handle-a-webhook">Sign, verify, and handle an HTTP event idempotently.</Card>
  <Card title="Schedule a job" href="/build/schedule-a-job">Run code on a schedule, then push and deploy it.</Card>
  <Card title="Workflows quickstart" href="/build/workflows/quickstart">Build a run with an approval step and test it offline.</Card>
</Columns>
