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.

Workflow authors must still write deterministic code. Wippy limits workflow modules to those classified as Deterministic or Workflow and supplies replay-safe implementations for supported operations. 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.

When a workflow spawns children via 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