dagron/ docs
Start

Get started

From an empty directory to a workflow you can watch run, then a tour of every screen in the console — what each region is, and which API call is behind it. If dagron is going to be the thing your team runs work on, this is the page to read first.

Bring up a stack

Everything on this page uses the Docker Compose quickstart, because it is the one that brings up the console — the web UI the rest of this page explains. One command, no configuration:

The whole stack — Postgres, engine, API, console
$ git clone https://github.com/lucheeseng827/dagron
$ cd dagron
$ docker compose -f compose.quickstart.yaml up -d
# podman compose works identically

# console at /, API at /api — one origin, one port
http://localhost:8080

That starts four things: postgres (the datastore, and the only state that exists), the engine (the reconcile loop and the executors — it also applies the migrations), dagron-api on :8080 (the authenticated gateway, which also serves the console), and a seed step that creates the admin user. Serving the pages from the same origin as /api is what makes the live updates work.

Two other ways in, if you do not want containers: a single binarydagron dev workflow.yaml gives you the engine and a Swagger UI on 127.0.0.1:8787 against a SQLite file, with no console — and Helm for Kubernetes. Both are on the Overview, and the workflow YAML is identical in all three.

Sign in

Open http://localhost:8080 and sign in with the admin the quickstart seeded:

Seeded admin (compose.quickstart.yaml)
email     admin@local
password  dagron-admin

dagron-api owns login and mints its own HS256 session cookie, so there is no identity provider to stand up for local work. The credentials come from DAGRON_ADMIN_EMAIL / DAGRON_ADMIN_PASSWORD in the compose file — change them there before anyone else can reach the port.

First look — the Overview

You land on Overview. It answers one question — is anything wrong? — before it shows you anything else, and everything on it is a live read, not a cached digest.

The dagron console Overview page: a needs-attention banner, four stat tiles, and panels for next scheduled runs, recent runs and GitOps repositories
Overview — scheduler health, what needs attention, and the last few runs.
  1. The navigation rail. Grouped rather than flat: the working set at the top (Overview, Workflows, Runs, Approvals, Submit, GitOps), then Operations — Datasets, Environments, Backfills, Dead letters, Metrics — then Account and Admin. The count beside Workflows is live (3 here), and the amber dot on GitOps marks a section with something to look at.
  2. Search (⌘K). One palette over workflows, runs and schedules. Run ids match by prefix, names by substring, so pasting the first eight characters of a run id finds it. Backed by GET /api/search.
  3. The Live lamp. Lit means this page is attached to the server-sent event stream (GET /api/events/stream) and is updating itself. Turn it off and the page stops re-rendering — useful when you are reading a list that keeps moving.
  4. Needs attention. The banner is the point of the page. It collapses failed runs, pending approvals, dead letters and out-of-sync repositories from the last 24 hours into one sentence. When it says nothing needs attention, as here, you can stop reading the screen.
  5. The four tiles. Active workflows (0 of 3, and how many carry a schedule), runs today split ok/failed, the 7-day success rate, and GitOps sync state. Each is a link into the filtered list behind it.
  6. Next scheduled runs / Recent runs / GitOps repos. The three things you would otherwise go looking for. Recent shows the six newest runs with a status lamp each — green for succeeded — and every row opens that run.
  7. The scheduler lamp. Pinned to the bottom of the rail because it is the one thing whose failure invalidates everything above it. Scheduler live with the active-run count means a node currently holds the leader lease and is dispatching work.

Author your first workflow

Click + New workflow. The editor opens with two views of one spec, switchable at the top and always in sync:

Pick Start from an example… to skip the blank page. The workflow shown in the screenshots below is this one — an extract, three transforms that run in parallel, and a load that waits for all three:

nightly-etl
name: nightly-etl
tasks:
  - name: extract
    command: ["sh", "-c", "echo 'extract: 1,284 rows from orders_raw'"]
    max_attempts: 3
    retry_delay_secs: 2

  - name: transform-orders
    command: ["sh", "-c", "echo transform orders"]
    depends_on: [extract]

  - name: transform-customers
    command: ["sh", "-c", "echo transform customers"]
    depends_on: [extract]

  - name: transform-inventory
    command: ["sh", "-c", "echo transform inventory"]
    depends_on: [extract]

  # waits for all three — that is the whole fan-in
  - name: load
    command: ["sh", "-c", "echo load"]
    depends_on: [transform-orders, transform-customers, transform-inventory]
    timeout_secs: 300

There is no scheduler DSL and no Python to import: depends_on is the whole graph language. Click Save and you are taken to the saved workflow, where ▶ Run, Sync to Git, Delete and the Schedules drawer appear.

The fields a task can carry

FieldYAML keyMeaning
Commandcommandargv to execute, e.g. ["sh","-c","echo hi"].
Docker imagedocker_imageImage to run the command in. Empty = the host executor.
Depends ondepends_onTask names that must finish first — the DAG edges.
Max attemptsmax_attemptsAttempts on failure. Default 1, meaning no retry.
Retry delayretry_delay_secsBase backoff; the actual wait is base · 2^(attempt−1), clamped by retry_max_delay_secs.
Timeouttimeout_secsPer-task wall-clock budget.
Run whentrigger_ruleWhen this task fires given its dependencies' outcomes: all_success (default), all_done, one_failed, all_failed, none_failed. This is how you attach a cleanup or a failure handler.
Runs workflowworkflow_refRun another saved workflow as this step — a DAG of DAGs.
Calls templatetemplate + argumentsCall a reusable sub-DAG declared under templates: in the same spec.
Approval gatetype: approvalNo command: the run parks until a human approves or rejects it. Optional approval_timeout_secs with approval_on_timeout: reject (default) or approve.

Two more live at the top of the spec beside name: run_timeout_secs cancels the whole run past a wall-clock budget, and result_from nominates the task whose output becomes the run's result — which is what a synchronous caller gets back from /wait.

When the Visual tab is disabled. The canvas draws one node per task, so a spec using with_items: (one task fanned out into many), hook:, type: wait sensors or cache: would be drawn as a picture that lies about what runs. The editor greys the Visual tab out and names the fields responsible rather than let you edit that drawing. Nothing is lost — the spec is untouched and fully editable as YAML.

The Workflows list

Saved workflows live under Workflows. This is the inventory view — one row per definition, with enough history in it to see which ones are healthy without opening any.

The dagron Workflows list: three saved definitions in a table with schedule, last run, a 14-run history strip and success rate
Workflows — saved definitions, their schedules, and a 14-run history strip each.
  1. The subtitle counts what you have — definitions, how many carry an active schedule, and how many arrived from a connected Git repository rather than this editor.
  2. Table or Board. The same rows as a dense table, or as status cards. Both read GET /api/workflows, which returns each row already enriched with its schedule and a recent-run digest, so the list is one request rather than one per workflow.
  3. Search and the state filters. All / Active / Paused / GitOps. A workflow's state is a first-class field: paused and retired both refuse to start runs, but keep their schedules — which is almost always what someone reaching for Delete actually wanted.
  4. The badge beside each name is how the workflow gets triggered — Manual here, a cron expression where one is set. The line under it is the spec's description.
  5. 14-run history. One bar per recent run, newest on the right, coloured by outcome. An intermittently failing workflow is visible here as a striped strip without opening anything.
  6. Row actions. Run now (), edit (), delete (). Delete is admin-only and cascades to that workflow's schedules — it is the one irreversible control in the console, which is why the API answers a non-admin with a message naming state: retired instead.

Run it and watch it

Click ▶ Run. You land on the run view, and this is the screen you will spend real time in. Nothing here polls: task state arrives over SSE as the engine writes it.

A dagron run detail view: the DAG drawn as nodes coloured by status, with a task side panel showing status, attempt and filtered log output
Run detail — the live DAG, per-task status, and the selected task's own output.
  1. The run header. Workflow name, the short run id (814ef5a3 — the prefix the search palette matches on), the run's status lamp, what triggered it, and the elapsed wall clock. Live here is this run's own stream, GET /api/runs/{id}/stream.
  2. Graph / Timeline / Logs. Three readings of one run: the dependency structure, the same tasks laid out against time — where you see what actually ran in parallel versus what queued — and every task's output merged into one attributed stream.
  3. Re-run. A menu, not a button: re-run the whole thing, re-run from a chosen task, or re-run with changes, which opens the stored spec in an editor. A run always keeps its own snapshot of the definition it was made from, so this is exact even if the workflow has been edited since.
  4. Layout direction. Top-down, left-right, or diagonal. A wide fan-out reads better left-right; a long chain reads better top-down.
  5. The graph. Start and the terminal node are the run's own boundaries, not tasks. Every real task is a node carrying its name, status and duration, and the node border is the status: green for succeeded here. The three transform-* tasks sit on one rank because they have the same single dependency — that is the parallelism, drawn.
  6. Zoom, fit and lock at the bottom left; the minimap at the bottom right keeps you oriented once a DAG outgrows the viewport, and stays coloured by status so you can see a red node off-screen.
  7. The task panel. Click any node. Status, which attempt it finished on, and that task's captured output. Clear + downstream resets this completed task and everything that depends on it, then lets the scheduler redo exactly that cone — the surgical recovery you want when one step was wrong and the twenty before it were not.
  8. The log filter. The input and the level chips (error, warn, info, debug, trace, plain) filter server-side, not in the browser. A run's captured output can be hundreds of megabytes; shipping all of it so the page can grep would be a download, not a filter. The same grammar works from curl and the SDKs — see the API page.

What the statuses mean

StatusWhat it means
pendingIn the run, dependencies not yet satisfied.
readyDependencies satisfied; waiting for a worker slot, a runner class, or a concurrency pool.
runningClaimed by a worker, which renews a heartbeat lease every 10 s — so an eight-hour step is never mistaken for a dead one.
awaiting_approvalAn approval gate is parked on a human. It appears in Approvals as a worklist across every run.
succeeded / failedTerminal. A failed task is retried while max_attempts allows; the attempt counter in the task panel is how many it took.
cancelledStopped by an operator, or by the run's own run_timeout_secs.

When something breaks, the recoveries are all on this screen: retry the one task, clear + downstream to redo a cone, re-run from a task to resume the failed frontier, or cancel the run. You can also triage a failed run — mark it acknowledged, resolved or ignored with a note — so that "we have decided not to care about this one" is recorded as a different answer from "fixed".

Find it again — Runs

Runs is every execution across every workflow, newest first. Overview answers is anything wrong; this answers what happened.

The dagron Runs list: every execution across all workflows with status, run id, start time, duration and trigger, filtered by chips and dropdowns
Runs — every execution, filtered by status, workflow, or what triggered it.
  1. Live / Archive. Two stores behind one screen. Live is the hot database; Archive reads runs the retention sweep has already exported and purged, so history outlives the hot window without keeping it in Postgres forever.
  2. + Submit runs ad-hoc YAML without saving a workflow first — the same path as POST /api/runs, with the Blocks rail available.
  3. Status chips and the two dropdowns map exactly onto the query parameters of GET /api/runs: ?status=, ?name=, ?trigger=. Whatever you can filter here you can filter from a script.
  4. The run id is the link, and the short prefix shown is enough to paste into ⌘K later.
  5. Trigger is derived, not stored: Manual, Schedule or Backfill, resolved from the schedule stamp or the backfill ledger. It is how you tell a run someone started by hand from one the clock started.
  6. Paging is offset-based (?limit=, ?offset=), default 100 per page and capped at 500 — the same bounds the API enforces.

Read the trend — Metrics

The per-run views tell you about one run. Metrics is the shape of the whole deployment over time.

The dagron Metrics page: runs per day and average run duration over 14 days, plus live runs-by-status, tasks-by-status and dead-letter counts
Metrics — runs per day, average duration, live status counts, and queue health.
  1. Runs per day · 14d, stacked by outcome — the legend names all four (succeeded, cancelled, failed, still running). The window is adjustable from 1 to 90 days via GET /api/metrics/timeseries?days=, and can be narrowed to a single workflow with &name= to get that workflow's own trend.
  2. Avg run duration · 14d, off the same buckets. This is the chart that catches the regression a success rate hides: everything still passes, it just takes twice as long.
  3. Runs by status and Tasks by status are live counts, not the 14-day window — GET /api/metrics. Tasks outnumber runs (22 against 6 here) because each run carries several.
  4. Dead letters is queue health: ingested work that failed its attempts and was parked rather than dropped. Non-zero is a number to act on — the Dead letters page lets you inspect the payload and redrive it into a fresh run once the cause is fixed.

This page is the console's own rollup. For Prometheus, scrape the engine's /metrics endpoint instead — process counters and live gauges in text format, documented on the API page.

The same thing without a browser

Every screen above is a client of the same public API, and nothing in the console is privileged. Sign in, submit, block until it finishes:

Submit a run and wait for it
$ API=http://localhost:8080

# 1. Sign in — or mint a personal access token once and skip this step
$ TOKEN=$(curl -s $API/api/login -H 'content-type: application/json' \
    -d '{"email":"admin@local","password":"dagron-admin"}' | jq -r .token)

# 2. Submit the YAML. Idempotency-Key makes a retry safe to send twice.
$ RUN=$(jq -Rs '{yaml: .}' < nightly-etl.yaml | curl -s $API/api/runs \
    -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
    -H "idempotency-key: nightly-$(date +%F)" --data-binary @- | jq -r .run_id)

# 3. Block server-side until it reaches a terminal state
$ curl -s "$API/api/runs/$RUN/wait?timeout_secs=120" \
    -H "authorization: Bearer $TOKEN" | jq
{ "run_id": "…", "status": "succeeded", "finished": true,
  "result": "load", "failure": null }

That is the whole synchronous shape: submit, wait, read the result. If it had failed, failure would carry the earliest-finished failed task, its attempt count and the tail of its output — the reason, in the response you are already holding, without a second call.

For anything beyond this — every endpoint, the auth model, the log filter grammar, SSE, the Python and TypeScript SDKs, and the MCP tools an agent calls — see the HTTP API reference.

Where to go next

Three things worth trying next in the console itself: add a Schedule from the editor's drawer and watch the trigger badge change from Manual to the cron expression; add an type: approval task and see the run park in Approvals; and connect a repository under GitOps so the definitions live in Git and arrive by pull request instead of by edit.