Packages
journey
0.10.56
0.10.58
0.10.57
0.10.56
0.10.55
0.10.54
0.10.53
0.10.52
0.10.51
0.10.50
0.10.49
0.10.48
0.10.47
0.10.46
0.10.45
0.10.44
0.10.43
0.10.41
0.10.40
0.10.39
0.10.38
0.10.37
0.10.36
0.10.35
0.10.34
0.10.33
0.10.32
0.10.31
0.10.30
0.10.29
0.10.28
0.10.27
0.10.26
0.10.25
0.10.24
0.10.23
0.10.22
0.0.9
retired
0.0.8
0.0.7
0.0.6
0.0.5
0.0.3
0.0.2
Journey is a library for defining and running durable workflows with persistence, reliability, and scalability.
Current section
Files
Jump to
Current section
Files
lib/examples/tick_once.livemd
<!-- livebook:{"persist_outputs":true} -->
# "tick_once" nodes
```elixir
# [Optional] Setting Build Key, see https://gojourney.dev/your_keys
# (Using "Journey Livebook Demo" build key)
System.put_env("JOURNEY_BUILD_KEY", "B27AXHMERm2Z6ehZhL49v")
Mix.install(
[
{:ecto_sql, "~> 3.13"},
{:postgrex, "~> 0.22"},
{:jason, "~> 1.4"},
{:journey, "~> 0.10"},
{:kino, "~> 0.19"}
],
start_applications: false
)
Application.put_env(:journey, :log_level, :warning)
# Configure more frequent background sweeper runs (the default is 60 seconds).
# The precision of the timer is determined by the granularity of the sweeper.
Application.put_env(:journey, :background_sweeper, period_seconds: 5)
# This livebook requires a PostgreSQL database.
# If you don't have one running, you can start one with Docker:
# docker run --rm --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres:16
# Update this configuration to point to your database server
Application.put_env(:journey, Journey.Repo,
database: "journey_tick_once_nodes",
username: "postgres",
password: "postgres",
hostname: "localhost",
log: false,
port: 5432
)
Application.put_env(:journey, :ecto_repos, [Journey.Repo])
Journey.Repo.__adapter__().storage_up(Journey.Repo.config())
Application.loaded_applications()
|> Enum.map(fn {app, _, _} -> app end)
|> Enum.each(&Application.ensure_all_started/1)
```
## DB Setup
This livebook requires a PostgreSQL service. If you don't have one running, you can start one with Docker:
```bash
docker run --rm --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres:16
```
## What We'll Cover
This tutorial focuses on the `tick_once` node type, which is used for scheduling future events.
A `tick_once` node computes a future timestamp (in unix seconds).
Its downstream nodes become unblocked and fire when that future time arrives.
In this livebook, we'll build a nap scheduler: spin up an execution, tell it your name, `tick_once` will compute the scheduled nap time, and a `compute` node fires when the time arrives.
We will also look at the execution's values, diagram, and detailed introspection.
## Define the Graph
```elixir
import Journey.Node
schedule_nap_in_seconds = 30
graph = Journey.new_graph(
"Nap Scheduler",
"v1",
[
input(:name),
tick_once(:schedule_nap, [:name],
fn %{name: name} ->
IO.puts("schedule_nap: scheduling a nap for #{name} in #{schedule_nap_in_seconds} seconds")
{:ok, System.os_time(:second) + schedule_nap_in_seconds}
end
),
compute(:start_nap, [:name, :schedule_nap],
fn %{name: name} ->
{:ok, "time for a nap, #{name}!"}
end
)
]
); :ok
```
<!-- livebook:{"output":true} -->
```
:ok
```
Three nodes:
* `:name` is an input node, setting it kicks everything off,
* `:schedule_nap` fires as soon as `:name` is set, and returns the time (unix epoch seconds) at which the nap should start, and
* `:start_nap` fires when the time returned by `:schedule_nap` has arrived.
<!-- livebook:{"break_markdown":true} -->
Visualize the graph:
```elixir
graph
|> Journey.Tools.generate_mermaid_graph()
|> Kino.Mermaid.new()
```
<!-- livebook:{"output":true} -->
```mermaid
graph TD
%% Graph
subgraph Graph["π§© 'Nap Scheduler', version v1"]
execution_id[execution_id]
last_updated_at[last_updated_at]
name[name]
schedule_nap[["schedule_nap<br/>(anonymous fn)<br/>tick_once node"]]
start_nap[["start_nap<br/>(anonymous fn)"]]
name --> schedule_nap
name --> start_nap
schedule_nap --> start_nap
end
%% Styling
classDef defaultNode fill:#f8f9fa,stroke:#495057,stroke-width:2px,color:#000000
%% Apply styles to nodes
class execution_id,last_updated_at,name,schedule_nap,start_nap defaultNode
```
## Start an Execution and Request a Nap
```elixir
execution =
graph
|> Journey.start()
|> Journey.set(:name, "Luigi");
:ok
```
<!-- livebook:{"output":true} -->
```
:ok
```
## Schedule That Nap
The tick fires immediately, but it's not time to start a nap just yet:
```elixir
{:ok, scheduled_time, revision} = Journey.get(execution, :schedule_nap, wait: :any)
now = System.os_time(:second)
scheduled_at_string = scheduled_time |> DateTime.from_unix!() |> Calendar.strftime("%H:%M:%S UTC")
"scheduled_time: #{scheduled_at_string}, in #{scheduled_time-now} seconds"
```
<!-- livebook:{"output":true} -->
```
schedule_nap: scheduling a nap for Luigi in 30 seconds
```
<!-- livebook:{"output":true} -->
```
"scheduled_time: 05:17:13 UTC, in 29 seconds"
```
`:schedule_nap` has computed β its value is a unix timestamp, containing the scheduled time.
`:start_nap` hasn't fired yet. It's waiting for the scheduled time:
```elixir
execution.id
|> Journey.Tools.generate_mermaid_execution()
|> Kino.Mermaid.new()
```
<!-- livebook:{"output":true} -->
```mermaid
graph TD
%% Graph
subgraph Graph["π§© 'Nap Scheduler', version v1, EXECL4Y00T46HD7MJH72R6A0"]
execution_id["β
execution_id"]
last_updated_at["β
last_updated_at"]
name["β
name"]
schedule_nap[["β
schedule_nap<br/>(anonymous fn)<br/>tick_once node"]]
start_nap[["π« start_nap<br/>(anonymous fn)"]]
name --> schedule_nap
name --> start_nap
schedule_nap --> start_nap
end
%% Styling
classDef setNode fill:#e1f5fe,stroke:#01579b,stroke-width:2px,color:#000000
classDef computingNode fill:#fff8e1,stroke:#f57f17,stroke-width:2px,color:#000000
classDef errorNode fill:#f8bbd0,stroke:#b71c1c,stroke-width:2px,color:#000000
classDef neutralNode fill:#f8f9fa,stroke:#495057,stroke-width:2px,color:#000000
%% Apply styles to nodes
class schedule_nap,name,last_updated_at,execution_id setNode
class start_nap neutralNode
```
## Wait for Nap Time to Start
```elixir
"Waiting for #{scheduled_at_string}..."
```
<!-- livebook:{"output":true} -->
```
"Waiting for 05:17:13 UTC..."
```
This will block until it's time:
```elixir
{:ok, value, _revision} = Journey.get(execution, :start_nap, wait: {:newer_than, revision}, timeout: 60_000)
now = DateTime.utc_now() |> Calendar.strftime("%H:%M:%S UTC")
"#{now}: #{value}"
```
<!-- livebook:{"output":true} -->
```
"05:17:17 UTC: time for a nap, Luigi!"
```
The actual event might occur a few seconds after its scheduled time. The precision of the timer is determined by the configured granularity of the sweeper βΒ see `background_sweeper` configuration in the setup section.
<!-- livebook:{"break_markdown":true} -->
Examining the execution, we can see that `:start_nap` has been "computed."
```elixir
Journey.values(execution)
```
<!-- livebook:{"output":true} -->
```
%{
name: "Luigi",
last_updated_at: 1776921436,
execution_id: "EXECL4Y00T46HD7MJH72R6A0",
schedule_nap: 1776921433,
start_nap: "time for a nap, Luigi!"
}
```
```elixir
execution.id
|> Journey.Tools.generate_mermaid_execution()
|> Kino.Mermaid.new()
```
<!-- livebook:{"output":true} -->
```mermaid
graph TD
%% Graph
subgraph Graph["π§© 'Nap Scheduler', version v1, EXECL4Y00T46HD7MJH72R6A0"]
execution_id["β
execution_id"]
last_updated_at["β
last_updated_at"]
name["β
name"]
schedule_nap[["β
schedule_nap<br/>(anonymous fn)<br/>tick_once node"]]
start_nap[["β
start_nap<br/>(anonymous fn)"]]
name --> schedule_nap
name --> start_nap
schedule_nap --> start_nap
end
%% Styling
classDef setNode fill:#e1f5fe,stroke:#01579b,stroke-width:2px,color:#000000
classDef computingNode fill:#fff8e1,stroke:#f57f17,stroke-width:2px,color:#000000
classDef errorNode fill:#f8bbd0,stroke:#b71c1c,stroke-width:2px,color:#000000
classDef neutralNode fill:#f8f9fa,stroke:#495057,stroke-width:2px,color:#000000
%% Apply styles to nodes
class start_nap,schedule_nap,name,last_updated_at,execution_id setNode
```
```elixir
Journey.Tools.introspect(execution.id) |> IO.puts()
```
<!-- livebook:{"output":true} -->
```
Execution summary:
- ID: 'EXECL4Y00T46HD7MJH72R6A0'
- Graph: 'Nap Scheduler' | 'v1'
- Archived at: not archived
- Created at: 2026-04-23 05:16:43Z UTC | 34 seconds ago
- Last updated at: 2026-04-23 05:17:16Z UTC | 1 seconds ago
- Duration: 33 seconds
- Revision: 5
- # of Values: 5 (set) / 5 (total)
- # of Computations: 2
Values:
- Set:
- last_updated_at: '1776921436' | :input
set at 2026-04-23 05:17:16Z | rev: 5
- start_nap: '"time for a nap, Luigi!"' | :compute
computed at 2026-04-23 05:17:16Z | rev: 5
- schedule_nap: '1776921433' | :tick_once
computed at 2026-04-23 05:16:43Z | rev: 3
- name: '"Luigi"' | :input
set at 2026-04-23 05:16:43Z | rev: 1
- execution_id: 'EXECL4Y00T46HD7MJH72R6A0' | :input
set at 2026-04-23 05:16:43Z | rev: 0
- Not set:
Computations:
- Completed:
- :start_nap (CMPXDL51R4XEZD24YR7JG0D): β
:success | :compute | rev 5
started: 2026-04-23 05:17:16Z | completed: 2026-04-23 05:17:16Z (0s)
inputs used:
:name (rev 1)
:schedule_nap (rev 3)
- :schedule_nap (CMP4A4LTR1L38R80X9RATGJ): β
:success | :tick_once | rev 3
started: 2026-04-23 05:16:43Z | completed: 2026-04-23 05:16:43Z (0s)
inputs used:
:name (rev 1)
- Outstanding:
```
<!-- livebook:{"output":true} -->
```
:ok
```
## Beyond Naps
`tick_once` is useful when you need to schedule an action at a specific future time. Things like:
* issue a reminder a day before the due date,
* send the user a reminder email 2 weeks after their last visit,
* archive user information 1 month after their last engagement,
* ... any other place where you need to schedule a future event.
See the `tick_once/4` documentation for more details and examples.
## Summary
In this Livebook, we saw how `tick_once` nodes schedule actions for a specific future time.
We built a nap scheduler with three nodes β an `input`, a `tick_once`, and a `compute` β and watched the compute node wait until the scheduled time before firing.
Key takeaways:
* A `tick_once` node computes immediately, but its value is a future timestamp (in unix seconds).
* Downstream nodes will be unblocked when that time arrives.
* Use `tick_once` for reminders, delayed actions, scheduled cleanup β any time something should happen at a later point.