Ship runbooks,
not shell in YAML
An Elixir toolkit for building, executing, and managing multi-step automation as a DAG — with automatic checkpointing so execution resumes exactly where it left off.
Define once, run anywhere
Runbooks are Elixir modules
defmodule MyApp.Runbooks.Deploy do
use Runcom.Runbook, name: "deploy"
require Runcom.Steps.GetUrl, as: GetUrl
require Runcom.Steps.Unarchive, as: Unarchive
require Runcom.Steps.Systemd, as: Systemd
require Runcom.Steps.WaitFor, as: WaitFor
@impl true
def build(params) do
Runcom.new("deploy-#{params.version}",
name: "Deploy v#{params.version}"
)
|> GetUrl.add("download",
url: "https://releases.example.com/myapp-#{params.version}.tar.gz",
dest: "/opt/myapp/release.tar.gz"
)
|> Unarchive.add("extract",
src: "/opt/myapp/release.tar.gz",
dest: "/opt/myapp/current",
await: ["download"]
)
|> Systemd.add("restart",
name: "myapp",
state: :restarted,
await: ["extract"]
)
|> WaitFor.add("healthcheck",
tcp_port: 4000,
timeout: 30_000,
await: ["restart"]
)
end
end
Why Runcom
The problems you keep solving by hand
Checkpoint & Resume
Every step is checkpointed to disk. Execution survives reboots, crashes, and restarts — skipping finished work without re-evaluating it.
DAG Execution
Steps declare explicit dependencies and run in parallel where possible. Failed steps cause dependents to skip, not cascade.
Elixir DSL
Full programmatic control — not YAML templates. Runbooks are modules with schemas, compile-time validation, and pattern matching.
Composable
Runbooks can be nested, grafted, and merged. Build complex workflows from reusable building blocks.
Live Observability
Phoenix LiveView dashboard shows real-time execution progress, step output, and dispatch status across your fleet.
Bash Static Analysis
The ~BASH
sigil parses shell scripts at compile time — missing
fi
or unmatched quotes fail the build, not the deploy.
Architecture
Server ↔ Agent, transport pluggable
Ships with RabbitMQ via Broadway. Bring your own transport by implementing the behaviour.
Server
runcom_web
LiveView Dashboard
runcom_rmq
Broadway Consumers
runcom_ecto
Postgres Store
runcom
Core DSL
RabbitMQ
Agent
runcom
Core DSL + Executor
runcom_rmq
Client
Write your own
Custom steps are just modules
defmodule MyApp.Steps.RotateLogs do
use Runcom.Step, name: "Rotate Logs"
import Bash.Sigil
# Parsed at compile time — a missing `fi`
# fails the build, not the deploy.
@rotate ~BASH"""
set -e
if [ $(du -sm "$LOG_DIR" | cut -f1) -gt "$MAX_MB" ]; then
tar czf "$LOG_DIR/archive-$(date +%s).tar.gz" \
"$LOG_DIR"/*.log
truncate -s 0 "$LOG_DIR"/*.log
echo "rotated"
else
echo "skipped"
fi
"""
schema do
field :log_dir, :string, default: "/var/log/myapp"
field :max_mb, :integer, default: 500
end
@impl true
def run(_rc, opts) do
env = %{
"LOG_DIR" => opts.log_dir,
"MAX_MB" => to_string(opts.max_mb)
}
case Bash.run(@rotate, env: env) do
{output, 0} ->
{:ok, Result.ok(output: String.trim(output))}
{output, code} ->
{:error, Result.error(
output: output, exit_code: code
)}
end
end
end
defmodule MyApp.Steps.NotifyDeploy do
use Runcom.Step, name: "Notify Deploy"
schema do
field :channel, :string, default: "#deploys"
end
@impl true
def run(rc, opts) do
# Read results from earlier steps
download = Runcom.result(rc, "download")
restart = Runcom.result(rc, "restart")
message =
case restart.status do
:ok ->
"Deployed #{rc.assigns.version} " <>
"(#{download.duration_ms}ms download)"
:error ->
"Deploy #{rc.assigns.version} failed " <>
"at restart: #{restart.error}"
end
MyApp.Slack.post(opts.channel, message)
{:ok, Result.ok(output: message)}
end
end
Batteries included
20+ built-in steps
Steps are composable behaviours — add your own by implementing Runcom.Step .
Packages
Pick what you need
| Package | Version | Description |
|---|---|---|
| runcom | 0.1.0 | Core DSL, step behaviours, execution engine, and checkpointing |
| runcom_ecto | 0.1.0 | Ecto/Postgres persistence — results, dispatches, secrets, analytics |
| runcom_web | 0.1.0 | Phoenix LiveView dashboard, visual builder, dispatch UI, and metrics |
| runcom_rmq | 0.1.0 | RabbitMQ transport — sync, events, and dispatch via Broadway |
defp deps do
[
{:runcom, "~> 0.1.0"},
{:runcom_ecto, "~> 0.1.0"},
{:runcom_web, "~> 0.1.0"},
{:runcom_rmq, "~> 0.1.0"}
]
end
defp deps do
[
{:runcom, "~> 0.1.0"},
{:runcom_rmq, "~> 0.1.0"}
]
end
Ready to ship runbooks?
Read the docs, clone the demo, or jump straight into code.