# Resilient, Scalable Applications, Built Simply

Journey is an Elixir package for defining and running durable workflows, with PostgreSQL persistence, horizontal scalability, orchestration, retries, crash recovery, scheduling, introspection and analytics.

Journey scales with your application. No cloud solutions to subscribe to or depend on. Nothing new to deploy and operate in your infrastructure. Your data stays with you, in your PostgreSQL database.

# Durable Workflow: An Example

The example below illustrates:

  • defining a workflow,
  • running a durable execution of the workflow,
  • introspecting an in-flight execution to understand its state,
  • loading and resuming an execution after an outage / restart / page reload /..., and
  • collecting analytics.

## Defining a Workflow as a Graph

This graph defines a workflow with two input nodes (:name and :email_address), and one computed node (:greeting, whose function computes and prints a welcome message, once :name and :email_address become available):

iex> import Journey.Node

iex> graph = Journey.new_graph(
  "Onboarding",
  "v1",
  [
    input(:name),
    input(:email_address),
    compute(
      :greeting,
      [:name, :email_address],
      fn values ->
        welcome = "Welcome, #{values.name} at #{values.email_address}"
        IO.puts(welcome)
        {:ok, welcome}
      end)
  ]
)

As a side note, here are some examples of things that can lend themselves to being implemented as durable workflows:

  • a User in a project management system, with their name, preferences, email address, timezone, and scheduled weekly summary emails,
  • a Ticket in a project management system, with its description, assignees, and notifications,
  • a Web Site Visit Session: the state of a visitor's web site activity (pages visited, inputs, "come back later" notifications),
  • a Delivery trip in a food delivery service (e.g. JourDash (jourdash.demo.gojourney.dev) (opens in a new window) ),
  • a Useless Machine that turns itself off,
  • a pipeline for evaluating incoming resumes,
  • a credit card application process.

## Running an execution of the graph:

Now that we have defined the graph, we can run its durable executions, setting the values of the input nodes, and getting the computed value:

(some output omitted for brevity)

iex> execution =
  graph
  |> Journey.start()
  |> Journey.set(:name, "Mario")
  |> Journey.set(:email_address, "mario@example.com")

"Welcome, Mario at mario@example.com"

iex> Journey.values(execution) |> IO.inspect()
%{
  name: "Mario",
  email_address: "mario@example.com",
  greeting: "Welcome, Mario at mario@example.com"
}

Each of the values is persisted to PostgreSQL as soon as it is set (:name, :email_address) or computed (:greeting).

The function that computes and prints the greeting executes reliably (with retries, if needed), on any of the replicas of the application, across system restarts, re-deployments, and infrastructure outages.

## Introspecting an in-flight execution

In this execution, Luigi has yet to be greeted.

iex> execution =
  graph
  |> Journey.start()
  |> Journey.set(:name, "Luigi")

iex> Journey.values(execution)
%{
  name: "Luigi"
}

Journey's tooling can help us understand why:

iex> execution.id
  |> Journey.Tools.introspect()
  |> IO.puts()

"""
Values:
- Set:
  - name: '"Luigi"' | :input
    set at 2025-11-13 07:25:15Z | rev: 1


- Not set:
  - email_address: <unk> | :input
  - greeting: <unk> | :compute

Computations:
- Completed:


- Outstanding:
  - greeting: ⬜ :not_set (not yet attempted) | :compute
      :and
        ├─ ✅ :name | &provided?/1 | rev 1
        └─ 🛑 :email_address | &provided?/1
"""

Ah! :greeting is blocked, because it's waiting for Luigi's :email_address!

## "My infrastructure is back after an outage. Can I resume executions?"

Sure! With Journey's durable executions, as long as you have the ID of the execution, you can simply reload the execution and continue – set input values, get computed values, etc. – as if nothing happened.

iex> execution.id |> IO.inspect()
"EXEC7BM701T4EGEG996X6BRY"

iex> execution = Journey.load("EXEC7BM701T4EGEG996X6BRY")

iex> Journey.set(execution, :email_address, "luigi@example.com")
"Welcome, Luigi at luigi@example.com"

iex> Journey.values(execution)
%{
  name: "Luigi",
  email_address: "luigi@example.com",
  greeting: "Welcome, Luigi at luigi@example.com"
}

Handling interruptions – infrastructure outages, re-deployments, scaling events, users reloading pages or returning after a break – is as easy as calling Journey.load/1.

## Analytics?

Business: "Is everyone getting greeted?"

Me: "Yes, 100%! Here's the analytics:"

iex> Journey.Insights.FlowAnalytics.flow_analytics(graph.name, graph.version)
     |> Journey.Insights.FlowAnalytics.to_text()
     |> IO.puts()
Graph: 'Welcome'
Version: 'v1'

EXECUTION STATS:
----------
Total executions: 2

NODE STATS (3 nodes):
----------
Node Name: 'email_address'
Type: input
Reached by: 2 executions (100%)
Average time to reach: 760 seconds
Flow ends here: 0 executions (0.0% of all, 0.0% of reached)

Node Name: 'greeting'
Type: compute
Reached by: 2 executions (100%)
Average time to reach: 760 seconds
Flow ends here: 0 executions (0.0% of all, 0.0% of reached)

Node Name: 'name'
Type: input
Reached by: 2 executions (100%)
Average time to reach: 8 seconds
Flow ends here: 0 executions (0.0% of all, 0.0% of reached)

## Learn More, Play With Real Apps

See /docs (opens in a new window) for full documentation, running demo applications, and source code.