Durable workflows for Elixir, on Temporal.
The shortest honest call:
greeting = Greet.execute!("Fresha")A workflow module declares what it is — behaviour, identity, address —
and use Temporalex.Workflow generates the call-side surface:
defmodule Greet do
use Temporalex.Workflow, queue: "greetings"
@impl true
def id(name), do: "greet-#{name}"
@impl true
def run(name), do: {:ok, "Hello, #{name}!"}
endThis module holds what happens between building a start and running it:
the chain steps that shape a Temporalex.Start, the terminal verbs that
perform it, and await for collecting a result later.
booking_id
|> Booking.new()
|> Temporalex.retry(max_attempts: 3)
|> Temporalex.fairness(salon_id)
|> Temporalex.index(salon_id: salon_id)
|> Temporalex.execute!()Builders are inert; terminal verbs run; a bang raises. A chain has exactly one terminal verb, at the end — everything before it is data.
See docs/rfcs/0002-client-surface.md for the full design.
Summary
Functions
Waits for the workflow's result.
Like await/2 but returns the result and raises on failure.
Selects a named client instead of the default one.
Cron schedule for a recurring workflow.
Like execute/1 but returns the result and raises on failure.
The whole chain's lifetime, retries and continue-as-new included. As
consequential as run_timeout/2, and as deliberately undefaulted.
Raises an application failure describing a business outcome.
Fair dispatch key, typically a tenant id — tasks sharing a key are dispatched in proportion to their weight, so one noisy tenant cannot monopolise a queue.
Header payloads — usually an interceptor's job, not the call site's.
Overrides the workflow id (id/1 normally derived it in new).
Indexed search attributes — machine-findable via
temporal workflow list --query. The unindexed, human-readable counterpart
is the memo.
Overrides the durable input — rare: when address and input diverge.
Queue priority band — smaller is higher.
Overrides the task queue — for starting on someone else's queue.
Workflow retry policy.
One run's lifetime. Consequential: expiry destroys the run without compensation — which is why there is no default.
Performs the start and returns {:ok, handle} — nobody waits.
Like start/1 but returns the handle and raises on failure.
How long a waiter waits. Expiring never touches the workflow.
Functions
@spec await( Temporalex.Client.Handle.t(), keyword() ) :: {:ok, term()} | {:error, Exception.t()}
Waits for the workflow's result.
A timeout is the caller giving up: the workflow keeps running and the
handle stays valid, so awaiting again later is legitimate. The wait bound
is opts[:timeout], else the timeout the start chain carried onto the
handle, else the client's workflow_result_timeout (60 seconds by
default).
@spec await!( Temporalex.Client.Handle.t(), keyword() ) :: term()
Like await/2 but returns the result and raises on failure.
@spec client(Temporalex.Start.t(), atom()) :: Temporalex.Start.t()
Selects a named client instead of the default one.
@spec cron(Temporalex.Start.t(), String.t()) :: Temporalex.Start.t()
Cron schedule for a recurring workflow.
@spec execute(Temporalex.Start.t()) :: {:ok, term()} | {:error, Exception.t()}
Performs the start and waits for the result: start/1 + await/2.
@spec execute!(Temporalex.Start.t()) :: term()
Like execute/1 but returns the result and raises on failure.
@spec execution_timeout(Temporalex.Start.t(), pos_integer()) :: Temporalex.Start.t()
The whole chain's lifetime, retries and continue-as-new included. As
consequential as run_timeout/2, and as deliberately undefaulted.
Raises an application failure describing a business outcome.
Temporalex.fail!("amount exceeds limit", type: "AmountTooLarge", retry: false)Called from an activity, this fails the current attempt. Temporal retries
it under the activity's retry policy unless retry: false. type: is the
stable string that retry policies and workflow matches key on.
Called from workflow code, this fails the workflow. That is final unless
the workflow was started with a retry policy, in which case retry: false
makes it final.
Options: type: (a non-empty string — the wire value Temporal matches
retry policies against; anything else, nil included, is refused rather
than silently replaced when encoded — omit the option to get the default
type), retry: (true or false, default true),
details:. Use Temporalex.Failure.application!/2 to set a nested
:cause.
@spec fairness(Temporalex.Start.t(), String.t() | integer(), float()) :: Temporalex.Start.t()
Fair dispatch key, typically a tenant id — tasks sharing a key are dispatched in proportion to their weight, so one noisy tenant cannot monopolise a queue.
@spec headers(Temporalex.Start.t(), keyword() | map()) :: Temporalex.Start.t()
Header payloads — usually an interceptor's job, not the call site's.
@spec id(Temporalex.Start.t(), String.t() | :generate) :: Temporalex.Start.t()
Overrides the workflow id (id/1 normally derived it in new).
@spec index(Temporalex.Start.t(), keyword() | map()) :: Temporalex.Start.t()
Indexed search attributes — machine-findable via
temporal workflow list --query. The unindexed, human-readable counterpart
is the memo.
@spec input(Temporalex.Start.t(), term()) :: Temporalex.Start.t()
Overrides the durable input — rare: when address and input diverge.
@spec priority(Temporalex.Start.t(), pos_integer()) :: Temporalex.Start.t()
Queue priority band — smaller is higher.
@spec queue(Temporalex.Start.t(), String.t()) :: Temporalex.Start.t()
Overrides the task queue — for starting on someone else's queue.
@spec retry( Temporalex.Start.t(), keyword() ) :: Temporalex.Start.t()
Workflow retry policy.
@spec run_timeout(Temporalex.Start.t(), pos_integer()) :: Temporalex.Start.t()
One run's lifetime. Consequential: expiry destroys the run without compensation — which is why there is no default.
@spec start(Temporalex.Start.t()) :: {:ok, Temporalex.Client.Handle.t()} | {:error, Exception.t()}
Performs the start and returns {:ok, handle} — nobody waits.
A duplicate start (same workflow id, still running) attaches to the
existing execution by default (id_conflict_policy: :use_existing); pass
id_conflict_policy: :fail on new/2 to make duplicates loud errors.
@spec start!(Temporalex.Start.t()) :: Temporalex.Client.Handle.t()
Like start/1 but returns the handle and raises on failure.
@spec timeout(Temporalex.Start.t(), pos_integer() | :infinity) :: Temporalex.Start.t()
How long a waiter waits. Expiring never touches the workflow.
On an execute! ending this bounds the call; on a start! ending it is
carried on the handle as the default for a later await!.