Skip to main content
A primitive is one way to make an agent do something, and there are eight: a tool in a skill, a webhook, a trigger, a job (or a dynamic job created at runtime), a workflow, a preprocessor or postprocessor, an MCP server, and a runtime call from your own code (Agents.invoke or Channels.send). 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

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

Two refinements. If the event comes from a SaaS you connected through Unified.to, subscribe to it with an integration webhook 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.

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

Add a tool

Write a tool, register its skill, and test it with lua test skill.

Handle a webhook

Sign, verify, and handle an HTTP event idempotently.

Schedule a job

Run code on a schedule, then push and deploy it.

Workflows quickstart

Build a run with an approval step and test it offline.