A task orchestration system designed to be efficient, fast and developer-friendly.
As a CTO and founder, I was tired of spending buckets of money to set up and manage Airflow, dealing with multiple databases, countless processes, Docker complexity, and of course its outdated and buggy UI. So we decided to build something that kept what we liked about Airflow and ditched what we didn’t. The result is Gust: a platform that’s 10× more efficient, faster, and far easier to set up.
Gust is the perfect fit for our needs, and I encourage you to try it and push it even further. There’s still plenty of room for improvements and new features. If you spot something or want to contribute an idea, don’t be shy! Drop an Issue or submit a PR.
defmodule HelloWorld do
@moduledoc false
# `schedule` and `on_finished_callback` are optional.
use Gust.DSL, schedule: "* * * * *", on_finished_callback: :notify_something
# Gust logs are stored and displayed through GustWeb via Logger.
require Logger
# Gust.Flows is used to query Dag, Run, and Task.
alias Gust.Flows
def notify_something(status, run) do
dag = Flows.get_dag!(run.dag_id)
message = "DAG: #{dag.name}; completed with status: #{status}"
Logger.info(message)
end
def skip_first_task?(%{run_id: run_id}) do
run = Flows.get_run!(run_id)
Map.get(run.params, "skip_first_task", false)
end
task :first_task, downstream: [:second_task], save: true, skip_if: :skip_first_task? do
greetings = "Hi from first_task"
Logger.info(greetings)
greetings = ["Hello!", "Olá!", "¡Hola!", "Bonjour!"]
# You can get secrets created on the Web UI
secret = Flows.get_secret_by_name("SUPER_SECRET")
if secret do
Logger.warning("I know your secret: #{secret.value}")
end
# The return value must be a map or a list when `save` is true.
greetings
end
task :second_task,
downstream: [:final_task],
ctx: %{params: params},
map_over: :first_task,
save: true do
message = "#{params["item"]} World!"
Logger.warning(message)
%{greeting: message}
end
task :final_task, ctx: %{run_id: run_id} do
# Getting tasks results
second_tasks = Flows.get_tasks_by_name("second_task", run_id)
Enum.each(second_tasks, fn task ->
Logger.warning(inspect(task.result))
end)
end
end

Want to try Gust quickly? Start with the Docker example. If you want full customization and extension, follow the instructions below to create a Gust app from scratch.
my_app for your app name and run:GUST_APP=my_app bash -c "$(curl -fsSL https://raw.githubusercontent.com/marciok/gust/main/setup_gust_app.sh)"
You can check what install script will perform here
Configure Postgres credentials on my_app/config/dev.exs
Run database setup:
mix ecto.createmix ecto.migrateRun Gust start:
mix phx.server
Check the docs on how to customize your DAG
Open "http://localhost:4000/gust/dags" to visualize your app
Gust can asynchronously report terminal task failures without interrupting DAG
execution. See the
Gust.DAG.Run.ErrorReporter
documentation for integration examples using Sentry or another error tracking
provider.
:map_over, creating one task instance per upstream list item.:skip_if; dependent downstream tasks are skipped when an upstream task is skipped.:wait_for, so a DAG can pause until another DAG, webhook, or external process resumes it.GustWeb includes a built-in MCP server that gives your LLM access to Gust’s core features, including listing DAGs, triggering runs, exploring DAG definitions, and debugging executions.
To mount it in your Phoenix router:
import GustWeb.MCPRouter
scope "/mcp", MyAppWeb do
pipe_through :api
gust_mcp_server()
end
The prefix comes from your MyAppWeb router scope, so you can also mount it
under a project-specific path to avoid clashes:
scope "/gust/mcp", MyAppWeb do
pipe_through :api
gust_mcp_server()
end
That would expose POST /gust/mcp/server. Keep auth and any app-specific
policy outside the macro, at the router scope or pipeline level.
claude mcp add --transport http gust-mcp http://localhost:4000/gust/mcp/servercodex mcp add gust-mcp --url http://localhost:4000/gust/mcp/serverInstall
gh skill install marciok/gust elixir-dag-creator
If you already have a Phoenix project and want to add Gust in place, install gust_web with Igniter.
mix local.hex --force
mix archive.install hex igniter_new --force
gust_web:mix igniter.install gust_web
It will mount the dashboard at /gust in your router, and create a dags/ folder.
Open dev.exs and set Gust.Repos credentials
mix ecto.create
mix ecto.migrate
mix phx.server
Open "http://localhost:4000/gust/dags".
You can run Gust with different runtime roles by setting GUST_ROLE:
core: runs the DAG pool and execution workers without the web UI.GUST_ROLE=core iex --sname core -S mix run --no-halt
web: runs the Phoenix server and loads DAG definitions for the UI, but does not execute DAGs.GUST_ROLE=web iex --sname web -S mix phx.server
console: loads DAG definitions and supporting runtime pieces for CLI or IEx work, but does not start DAG pooling workers.GUST_ROLE=console iex -S mix
mix gust.cli ... also defaults GUST_ROLE to console, and release builds ship a gust-cli wrapper that exports the same role automatically.
If you do not pass anything, Gust runs as single, which enables both the core and web behavior in the same node.
Choose the dispatch strategy by module. Use Gust.Run.Pooler for periodic
polling, or Gust.PGNotifier.Worker for PostgreSQL LISTEN/NOTIFY:
config :gust, run_dispatcher: Gust.Run.Pooler
# Or, without periodic polling:
config :gust, run_dispatcher: Gust.PGNotifier.Worker
The notification connection reuses Gust.Repo's database settings. Optional
connection-specific settings can be supplied separately, for example:
config :gust, :pg_notifications, reconnect_backoff: 2_000
Gust manages notification reconnection through its supervision tree, so
:sync_connect and :auto_reconnect overrides are ignored. Enqueuing and
notification happen in the same database transaction, and the claimer checks
the durable run queue once after every successful subscription. The PostgreSQL
dispatcher does not periodically poll the database.
You can find a full example here.
.env.example to .env.test:
cp .env.example .env.test
source .env.test
mix setup
MIX_ENV=test mix ecto.create
MIX_ENV=test mix ecto.migrate
mix test
mix test test/path/to/file_test.exs
mix test --failed
MIX_ENV=test mix coveralls.html --umbrella
connection refused: Postgres is not running or PGHOST/PGUSER/PGPASSWORD are incorrect.database "gust_rc_test" does not exist: run MIX_ENV=test mix ecto.create && MIX_ENV=test mix ecto.migrate.
Find the best offers and save money on car subscription service.
Gust is released under the MIT License.

Elixir
91.9%
HTML
4.3%
CSS
2.3%
A task orchestration system designed to be efficient, fast and developer-friendly.
As a CTO and founder, I was tired of spending buckets of money to set up and manage Airflow, dealing with multiple databases, countless processes, Docker complexity, and of course its outdated and buggy UI. So we decided to build something that kept what we liked about Airflow and ditched what we didn’t. The result is Gust: a platform that’s 10× more efficient, faster, and far easier to set up.
Gust is the perfect fit for our needs, and I encourage you to try it and push it even further. There’s still plenty of room for improvements and new features. If you spot something or want to contribute an idea, don’t be shy! Drop an Issue or submit a PR.
defmodule HelloWorld do
@moduledoc false
# `schedule` and `on_finished_callback` are optional.
use Gust.DSL, schedule: "* * * * *", on_finished_callback: :notify_something
# Gust logs are stored and displayed through GustWeb via Logger.
require Logger
# Gust.Flows is used to query Dag, Run, and Task.
alias Gust.Flows
def notify_something(status, run) do
dag = Flows.get_dag!(run.dag_id)
message = "DAG: #{dag.name}; completed with status: #{status}"
Logger.info(message)
end
def skip_first_task?(%{run_id: run_id}) do
run = Flows.get_run!(run_id)
Map.get(run.params, "skip_first_task", false)
end
task :first_task, downstream: [:second_task], save: true, skip_if: :skip_first_task? do
greetings = "Hi from first_task"
Logger.info(greetings)
greetings = ["Hello!", "Olá!", "¡Hola!", "Bonjour!"]
# You can get secrets created on the Web UI
secret = Flows.get_secret_by_name("SUPER_SECRET")
if secret do
Logger.warning("I know your secret: #{secret.value}")
end
# The return value must be a map or a list when `save` is true.
greetings
end
task :second_task,
downstream: [:final_task],
ctx: %{params: params},
map_over: :first_task,
save: true do
message = "#{params["item"]} World!"
Logger.warning(message)
%{greeting: message}
end
task :final_task, ctx: %{run_id: run_id} do
# Getting tasks results
second_tasks = Flows.get_tasks_by_name("second_task", run_id)
Enum.each(second_tasks, fn task ->
Logger.warning(inspect(task.result))
end)
end
end

Want to try Gust quickly? Start with the Docker example. If you want full customization and extension, follow the instructions below to create a Gust app from scratch.
my_app for your app name and run:GUST_APP=my_app bash -c "$(curl -fsSL https://raw.githubusercontent.com/marciok/gust/main/setup_gust_app.sh)"
You can check what install script will perform here
Configure Postgres credentials on my_app/config/dev.exs
Run database setup:
mix ecto.createmix ecto.migrateRun Gust start:
mix phx.server
Check the docs on how to customize your DAG
Open "http://localhost:4000/gust/dags" to visualize your app
Gust can asynchronously report terminal task failures without interrupting DAG
execution. See the
Gust.DAG.Run.ErrorReporter
documentation for integration examples using Sentry or another error tracking
provider.
:map_over, creating one task instance per upstream list item.:skip_if; dependent downstream tasks are skipped when an upstream task is skipped.:wait_for, so a DAG can pause until another DAG, webhook, or external process resumes it.GustWeb includes a built-in MCP server that gives your LLM access to Gust’s core features, including listing DAGs, triggering runs, exploring DAG definitions, and debugging executions.
To mount it in your Phoenix router:
import GustWeb.MCPRouter
scope "/mcp", MyAppWeb do
pipe_through :api
gust_mcp_server()
end
The prefix comes from your MyAppWeb router scope, so you can also mount it
under a project-specific path to avoid clashes:
scope "/gust/mcp", MyAppWeb do
pipe_through :api
gust_mcp_server()
end
That would expose POST /gust/mcp/server. Keep auth and any app-specific
policy outside the macro, at the router scope or pipeline level.
claude mcp add --transport http gust-mcp http://localhost:4000/gust/mcp/servercodex mcp add gust-mcp --url http://localhost:4000/gust/mcp/serverInstall
gh skill install marciok/gust elixir-dag-creator
If you already have a Phoenix project and want to add Gust in place, install gust_web with Igniter.
mix local.hex --force
mix archive.install hex igniter_new --force
gust_web:mix igniter.install gust_web
It will mount the dashboard at /gust in your router, and create a dags/ folder.
Open dev.exs and set Gust.Repos credentials
mix ecto.create
mix ecto.migrate
mix phx.server
Open "http://localhost:4000/gust/dags".
You can run Gust with different runtime roles by setting GUST_ROLE:
core: runs the DAG pool and execution workers without the web UI.GUST_ROLE=core iex --sname core -S mix run --no-halt
web: runs the Phoenix server and loads DAG definitions for the UI, but does not execute DAGs.GUST_ROLE=web iex --sname web -S mix phx.server
console: loads DAG definitions and supporting runtime pieces for CLI or IEx work, but does not start DAG pooling workers.GUST_ROLE=console iex -S mix
mix gust.cli ... also defaults GUST_ROLE to console, and release builds ship a gust-cli wrapper that exports the same role automatically.
If you do not pass anything, Gust runs as single, which enables both the core and web behavior in the same node.
Choose the dispatch strategy by module. Use Gust.Run.Pooler for periodic
polling, or Gust.PGNotifier.Worker for PostgreSQL LISTEN/NOTIFY:
config :gust, run_dispatcher: Gust.Run.Pooler
# Or, without periodic polling:
config :gust, run_dispatcher: Gust.PGNotifier.Worker
The notification connection reuses Gust.Repo's database settings. Optional
connection-specific settings can be supplied separately, for example:
config :gust, :pg_notifications, reconnect_backoff: 2_000
Gust manages notification reconnection through its supervision tree, so
:sync_connect and :auto_reconnect overrides are ignored. Enqueuing and
notification happen in the same database transaction, and the claimer checks
the durable run queue once after every successful subscription. The PostgreSQL
dispatcher does not periodically poll the database.
You can find a full example here.
.env.example to .env.test:
cp .env.example .env.test
source .env.test
mix setup
MIX_ENV=test mix ecto.create
MIX_ENV=test mix ecto.migrate
mix test
mix test test/path/to/file_test.exs
mix test --failed
MIX_ENV=test mix coveralls.html --umbrella
connection refused: Postgres is not running or PGHOST/PGUSER/PGPASSWORD are incorrect.database "gust_rc_test" does not exist: run MIX_ENV=test mix ecto.create && MIX_ENV=test mix ecto.migrate.
Find the best offers and save money on car subscription service.
Gust is released under the MIT License.

Elixir
91.9%
HTML
4.3%
CSS
2.3%