Workflows
Workflows persist the state of long-running operations so execution can recover after crashes and restarts. They suit processes such as payments, order fulfillment, and multi-step approvals.
Why Use Workflows
Functions keep in-flight state in memory, while workflows persist execution state:
| Aspect | Functions | Workflows |
|---|---|---|
| State | Call-local | Rebuilt from persisted history |
| Worker crash | In-flight call fails | Replays from recorded history |
| Duration | Seconds to minutes | Hours to months |
| Application failure | Returned to caller | Ends or retries according to provider policy |
How Workflows Work
Workflow code looks like regular Lua code:
local funcs = require("funcs")
local time = require("time")
local result, err = funcs.call("app.api:charge_card", payment)
if err then return nil, err end
time.sleep("24h")
local status, err = funcs.call("app.api:check_status", result.id)
if err then return nil, err end
if status == "failed" then
local _, refund_err = funcs.call("app.api:refund", result.id)
if refund_err then return nil, refund_err end
end
return status
The workflow engine intercepts calls and records their results. After a crash, it replays execution from the recorded history.
Inside a workflow, each funcs.call() target runs as a Temporal activity. A
target function.* entry must register with a worker through
meta.temporal.activity.worker; unregistered entries are not available to the
workflow. A process.* activity target additionally needs
meta.options.default_host (or the legacy meta.default_host) so it is
registered in the function registry used by the Temporal worker. See
Activities for the function activity example and
activity options.
funcs.call()
runs a recorded activity, time.sleep() uses a workflow timer,
uuid.v4() records a side effect, and time.now() reads the
workflow's deterministic time reference.
Workflow Patterns
Saga Pattern
Compensate on failure:
local funcs = require("funcs")
local inventory, err = funcs.call("app.inventory:reserve", items)
if err then return nil, err end
local payment, err = funcs.call("app.payments:charge", amount)
if err then
local _, compensation_err = funcs.call("app.inventory:release", inventory.id)
return nil, compensation_err or err
end
local shipping, err = funcs.call("app.shipping:create", order)
if err then
local _, refund_err = funcs.call("app.payments:refund", payment.id)
local _, release_err = funcs.call("app.inventory:release", inventory.id)
return nil, refund_err or release_err or err
end
return {inventory = inventory, payment = payment, shipping = shipping}
Waiting for Signals
Wait for external events (approval decisions, webhooks, user actions):
local funcs = require("funcs")
local _, err = funcs.call("app.approvals:submit", request)
if err then return nil, err end
local inbox = process.inbox()
local msg, open = inbox:receive() -- blocks until signal arrives
if not open then return nil, errors.new("workflow inbox closed") end
local decision, payload_err = msg:payload():data()
if payload_err then return nil, payload_err end
if decision.approved then
return funcs.call("app.orders:fulfill", request.order_id)
else
return funcs.call("app.notifications:send_rejection", request)
end
Choosing a Compute Model
| Use Case | Choose |
|---|---|
| HTTP request handling | Functions |
| Data transformation | Functions |
| Background jobs | Processes |
| User session state | Processes |
| Real-time messaging | Processes |
| Payment processing | Workflows |
| Order fulfillment | Workflows |
| Multi-day approvals | Workflows |
Starting Workflows
Workflows use process.spawn() with a workflow host:
-- Spawn workflow on temporal worker
local pid, err = process.spawn("app.workflows:order_processor", "app:temporal_worker", order_data)
if err then return nil, err end
-- Send signals to workflow
local ok, err = process.send(pid, "update", {status = "approved"})
if err then return nil, err end
return ok
The caller uses the same spawn API. The host determines whether the entry runs
on a temporal.worker or a process.host. Persisted history and replay apply
only to the Temporal-hosted path. A workflow entry run through an ordinary
process host has in-memory process semantics and does not gain Temporal
durability.
process.spawn(), they become child workflows on the same provider, maintaining durability guarantees.
Failure and Supervision
Processes can run as supervised services using process.service:
# Process definition
- name: session_handler
kind: process.lua
source: file://session_handler.lua
method: main
# Supervised service wrapping the process
- name: session_manager
kind: process.service
process: app:session_handler
host: app:processes
lifecycle:
auto_start: true
restart:
max_attempts: 10
Workflows do not use process supervision trees. The workflow provider manages persistence and recovery; application-level retries follow the configured workflow and activity policies.
Configuration
Workflow definition (spawned dynamically):
- name: order_processor
kind: workflow.lua
source: file://order_processor.lua
method: main
meta:
temporal:
workflow:
worker: app:temporal_worker
modules:
- funcs
- time
Every function or process invoked through funcs.call() also declares the
activity worker. For example:
- name: charge_card
kind: function.lua
source: file://charge_card.lua
method: main
meta:
temporal:
activity:
worker: app:temporal_worker
Workflow provider:
- name: temporal_worker
kind: temporal.worker
client: app:temporal_client
task_queue: "orders"
lifecycle:
auto_start: true
See Temporal for production workflow infrastructure.
See Also
- Functions — Request-scoped calls
- Process Model — Stateful background work
- Supervision — Process restart policies