Skip to content

REST API Reference ​

Browse the complete OpenAPI specification in Swagger UI.

Base Configuration ​

  • Base URL: http://localhost:8080/api/v1
  • Content-Type: application/json
  • OpenAPI Version: 3.0.0
  • Live OpenAPI Document: GET /api/v1/openapi.json

Authentication ​

The API supports multiple authentication methods:

  • API Keys: Include Authorization: Bearer dagu_<key> header (requires Builtin Auth)
  • Webhooks: Trigger endpoints use per-DAG webhook credentials. Send Authorization: Bearer dagu_wh_<token> (requires Builtin Auth)
  • JWT Token: Include Authorization: Bearer <jwt-token> header (from login endpoint)
  • Basic Auth: Include Authorization: Basic <base64(username:password)> header
  • No Authentication: When auth is disabled (default for local development)

API keys are recommended for general programmatic access as they support role-based permissions. See API Keys for details. For triggering specific DAGs from external systems, see Webhooks.

System Endpoints ​

Health Check ​

Endpoint: GET /api/v1/health

Checks the health status of the Dagu server.

Response (200):

json
{
  "status": "healthy",
  "version": "1.14.5",
  "uptime": 86400,
  "timestamp": "2024-02-11T16:30:45.123456789Z"
}

Response when unhealthy (503):

json
{
  "status": "unhealthy",
  "version": "1.14.5",
  "uptime": 300,
  "timestamp": "2024-02-11T16:30:45.123456789Z",
  "error": "Scheduler not responding"
}

Response Fields:

  • status: Server health status ("healthy" or "unhealthy")
  • version: Current server version
  • uptime: Server uptime in seconds
  • timestamp: Current server time

Get Scheduler Pause State ​

Endpoint: GET /api/v1/services/scheduler/pause

Reports whether scheduler-driven run creation is paused. Readable by any authenticated user.

Response (200):

json
{
  "paused": true,
  "pausedAt": "2024-02-11T16:30:45Z",
  "pausedBy": "admin",
  "reason": "database migration"
}

Response Fields:

  • paused: Whether the scheduler is currently paused
  • pausedAt: RFC3339 timestamp of the pause, omitted when running
  • pausedBy: User who paused the scheduler, omitted when running
  • reason: Operator-supplied explanation, omitted when running or not given

Pause or Resume the Scheduler ​

Endpoint: POST /api/v1/services/scheduler/pause

Pauses or resumes scheduled run creation for every DAG at once. Admin only.

Request Body:

json
{
  "paused": true,
  "reason": "database migration"
}
  • paused (required): Target state
  • reason (optional): Shown in the web UI banner while paused, up to 512 characters

Response (200): Success

Manual, webhook, and sub-DAG runs keep working while paused. Queued scheduled runs are aborted and catchup windows are discarded rather than deferred. See Pausing the Scheduler for the full behavior.

OpenAPI Document ​

Endpoint: GET /api/v1/openapi.json

Returns the normalized OpenAPI document served by this Dagu instance.

Notes:

  • When authentication is enabled, this endpoint is authenticated like the rest of the API.
  • The response advertises the mounted API path in servers[0].url such as /api/v1, /dagu/api/v1, or /dagu/rest.
  • The document includes the /openapi.json path itself, so clients can discover it from the schema.

DAG Management Endpoints ​

List DAGs ​

Endpoint: GET /api/v1/dags

Retrieves DAG definitions with optional filtering by name and labels.

Query Parameters:

ParameterTypeDescriptionDefault
pageintegerPage number (1-based)1
perPageintegerItems per page (max 1000)50
namestringFilter DAGs by name-
labelsstringFilter by labels. Comma-separated. Syntax: key, key=value, !key, key*, key=value*. Supports * and ? wildcards. AND logic.-
sortstringSort field: name, nextRun, status, or lastRunui.dags.sort_field (fallback name)
orderstringSort order: asc or descui.dags.sort_order (fallback asc)
remoteNodestringRemote node name"local"

Response (200):

json
{
  "dags": [
    {
      "fileName": "example",
      "dag": {
        "name": "example_dag",
        "group": "default",
        "schedule": [{"expression": "0 * * * *"}],
        "description": "Example DAG",
        "params": ["param1", "param2"],
        "defaultParams": "{}",
        "labels": ["example", "demo"]
      },
      "nextRun": "2026-03-29T09:30:00+09:00",
      "latestDAGRun": {
        "dagRunId": "20240101_120000",
        "name": "example_dag",
        "status": 1,
        "statusLabel": "running",
        "startedAt": "2024-01-01T12:00:00Z",
        "finishedAt": "",
        "log": "/logs/example_dag.log"
      },
      "suspended": false,
      "errors": []
    }
  ],
  "errors": [],
  "pagination": {
    "totalRecords": 45,
    "currentPage": 1,
    "totalPages": 5,
    "nextPage": 2,
    "prevPage": null
  }
}

schedule entries are recurring cron objects such as {"expression":"0 * * * *"} or typed one-off start entries such as {"kind":"at","at":"2026-03-29T09:30:00+09:00"}. nextRun is the scheduler-aware next planned run time for that DAG. For a pending one-off start entry, nextRun can remain visible even after that timestamp has passed, until the scheduler consumes it.

Schedule aliases in authored YAML are returned as canonical cron expressions. For example, "@daily" becomes "0 0 * * *" in schedule metadata.

sort=status orders DAGs by the status of their latest run: running, waiting, queued, failed, rejected, aborted, partially succeeded, then succeeded. sort=lastRun orders them by the start time of their latest run. Both sorts read every matching DAG, so the order holds across pages. DAGs without a run come last in either order, sorted by name. Above 300 matching DAGs, the list is sorted by name instead and errors says why. Pinned DAGs are listed first; the sort applies within the pinned and unpinned parts.

Create DAG ​

Endpoint: POST /api/v1/dags

Creates a new DAG file with the specified name. Optionally initializes it with a provided YAML specification.

Request Body:

json
{
  "name": "my-new-dag",
  "spec": "steps:\n  - run: echo hello"  // Optional - YAML spec to initialize the DAG
}

Notes:

  • If spec is provided, it will be validated before creation
  • If validation fails, returns 400 with error details
  • Without spec, creates a minimal DAG with a single echo step

Response (201):

json
{
  "name": "my-new-dag"
}

Error Response (400):

json
{
  "code": "bad_request",
  "message": "Invalid DAG name format"
}

Error Response (409):

json
{
  "code": "already_exists",
  "message": "DAG with this name already exists"
}

Get DAG Details ​

Endpoint: GET /api/v1/dags/{fileName}

Fetches detailed information about a specific DAG.

Path Parameters:

ParameterTypeDescriptionPattern
fileNamestringDAG file name^[a-zA-Z0-9_-]+$

Response (200):

json
{
  "dag": {
    "name": "data_processing_pipeline",
    "group": "ETL",
    "schedule": [
      {"expression": "0 2 * * *"},
      {"expression": "0 14 * * *"}
    ],
    "description": "Daily data processing pipeline for warehouse ETL",
    "env": [
      "DATA_SOURCE=postgres://prod-db:5432/analytics",
      "WAREHOUSE_URL=${env.WAREHOUSE_URL}"
    ],
    "log_dir": "/var/log/dagu/pipelines",
    "handler_on": {
      "success": {
        "name": "notify_success",
        "command": "notify.sh 'Pipeline completed successfully'"
      },
      "failure": {
        "name": "alert_on_failure",
        "command": "alert.sh 'Pipeline failed' high"
      },
      "exit": {
        "name": "cleanup",
        "command": "cleanup_temp_files.sh"
      }
    },
    "steps": [
      {
        "name": "extract_data",
        "id": "extract",
        "description": "Extract data from source database",
        "dir": "/app/etl",
        "command": "python",
        "args": ["extract.py", "--date", "${params.date}"],
        "stdout": "/logs/extract.out",
        "stderr": "/logs/extract.err",
        "output": "EXTRACTED_FILE",
        "preconditions": [
          {
            "condition": "test -f /data/ready.flag"
          }
        ]
      },
      {
        "name": "transform_data",
        "id": "transform",
        "description": "Apply transformations to extracted data",
        "command": "python transform.py --input=${env.EXTRACTED_FILE}",
        "depends": ["extract_data"],
        "output": "TRANSFORMED_FILE",
        "repeat_policy": {
          "repeat": false,
          "interval": 0
        },
        "mail_on_error": true
      },
      {
        "name": "load_to_warehouse",
        "id": "load",
        "run": "warehouse-loader",
        "params": "{\"file\": \"${env.TRANSFORMED_FILE}\", \"table\": \"fact_sales\"}",
        "depends": ["transform_data"]
      }
    ],
    "delay": 30,
    "hist_retention_days": 30,
    "preconditions": [
      {
        "eval": "$(date +%u)",
        "expected": "re:[1-5]"
      }
    ],
    "max_active_runs": 1,
    "max_active_steps": 5,
    "params": ["date", "env", "batch_size"],
    "paramDefs": [
      {
        "name": "date",
        "type": "string",
        "description": "Pipeline run date",
        "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
      },
      {
        "name": "env",
        "type": "string",
        "default": "dev",
        "enum": ["dev", "staging", "prod"]
      },
      {
        "name": "batch_size",
        "type": "integer",
        "default": 1000,
        "minimum": 1,
        "maximum": 10000
      }
    ],
    "paramSchema": {
      "type": "object",
      "required": ["env"],
      "properties": {
        "env": {
          "type": "string",
          "enum": ["dev", "staging", "prod"]
        },
        "batch_size": {
          "type": "integer",
          "default": 1000,
          "minimum": 1,
          "maximum": 10000
        }
      }
    },
    "defaultParams": "env=\"dev\" batch_size=\"1000\"",
    "labels": ["production", "etl", "daily"]
  },
  "localDags": [
    {
      "name": "warehouse-loader",
      "dag": {
        "name": "warehouse_loader_subdag",
        "steps": [
          {
            "name": "validate_schema",
            "command": "validate_schema.py"
          },
          {
            "name": "load_data",
            "command": "load_to_warehouse.py",
            "depends": ["validate_schema"]
          }
        ]
      },
      "errors": []
    }
  ],
  "latestDAGRun": {
    "rootDAGRunName": "data_processing_pipeline",
    "rootDAGRunId": "20240211_140000_abc123",
    "parentDAGRunName": "",
    "parentDAGRunId": "",
    "dagRunId": "20240211_140000_abc123",
    "name": "data_processing_pipeline",
    "status": 4,
    "statusLabel": "succeeded",
    "queuedAt": "",
    "startedAt": "2024-02-11T14:00:00Z",
    "finishedAt": "2024-02-11T14:45:30Z",
    "log": "/logs/data_processing_pipeline/20240211_140000_abc123.log",
    "params": "{\"date\": \"2024-02-11\", \"env\": \"production\", \"batch_size\": 5000}"
  },
  "suspended": false,
  "errors": []
}

paramSchema is present when Dagu can safely render a schema-backed object form directly in the start/enqueue UI. Clients should prefer paramSchema over paramDefs when both are present.

paramDefs is additive metadata derived from DAG-level params:. Typed clients can use it to render scalar controls when paramSchema is absent. paramDefs can be derived from inline rich params, top-level inline JSON Schema, and representable external schemas.

The DAG object also exposes histRetentionDays for DAG history retention.

defaultParams is the resolved shell-style default parameter string, not JSON. For example: environment="staging" batch_size="25".

When an inline rich param uses eval, paramDefs.default remains only the authored literal default, if one exists. Clients should not evaluate template defaults locally.

The DAG object also carries upcomingRuns: the next ten fire times of its start schedules, earliest first. Each entry has time, the scheduleIndex of its schedule entry, and the timezone it runs in, plus businessDay, extraBusinessDay, and skippedHolidays when a business calendar placed it. The list comes from the definition alone; upcomingRunsHold is suspended or schedulerPaused when none of the runs will start. GET /dags/{fileName}/spec and POST /dags/validate return the same fields.

Delete DAG ​

Endpoint: DELETE /api/v1/dags/{fileName}

Permanently removes a DAG definition from the system.

Response (204): No content

Error Response (404):

json
{
  "code": "not_found",
  "message": "DAG not found"
}

Error Response (403):

json
{
  "code": "forbidden",
  "message": "Permission denied to delete DAGs"
}

Get DAG Specification ​

Endpoint: GET /api/v1/dags/{fileName}/spec

Fetches the YAML specification of a DAG.

Response (200):

json
{
  "dag": {
    "name": "example_dag"
  },
  "spec": "name: example_dag\nsteps:\n  - name: hello\n    run: echo Hello",
  "sha256": "4f2c0e7b9d1a...",
  "errors": []
}

sha256 is the lowercase hex SHA-256 of the file bytes in spec.

Update DAG Specification ​

Endpoint: PUT /api/v1/dags/{fileName}/spec

Updates the YAML specification of a DAG.

Request Body:

json
{
  "spec": "name: example_dag\nsteps:\n  - name: hello\n    run: echo Hello World",
  "expectedSha256": "4f2c0e7b9d1a..."
}

expectedSha256 is optional. Set it to the sha256 of the spec that was edited. When the file has changed since, the server writes nothing and returns 409.

Response (200):

json
{
  "errors": [],
  "sha256": "9b8e61d0c3f4..."
}

sha256 is the SHA-256 of the bytes written.

Response with Validation Errors (200):

json
{
  "errors": [
    "Line 5: Invalid step configuration - missing command",
    "Line 10: Circular dependency detected between step1 and step2"
  ]
}

Error Response (403):

json
{
  "code": "forbidden",
  "message": "Permission denied to edit DAGs"
}

Error Response (409) - The current file does not match expectedSha256:

json
{
  "code": "conflict",
  "message": "DAG example_dag changed since it was read: expected sha256 4f2c0e7b9d1a..., current sha256 7a1d93e05b2c..."
}

Validate DAG Specification ​

Endpoint: POST /api/v1/dags/validate

Validates a DAG YAML specification without persisting any changes. Returns a list of validation errors. When the spec can be partially parsed, the response may include parsed DAG details built with error-tolerant loading.

Request Body:

json
{
  "spec": "steps:\n  - name: step1\n    run: echo hello\n  - name: step2\n    run: echo world\n    depends: step1",
  "name": "optional-dag-name"  // Optional - name to use when spec omits a name
}

Response (200):

json
{
  "valid": true,
  "errors": [],
  "dag": {
    "name": "example-dag",
    "group": "default",
    "description": "Validated DAG",
    "schedule": [],
    "params": [],
    "defaultParams": "{}",
    "labels": []
  }
}

Response with errors (200):

json
{
  "valid": false,
  "errors": [
    "Step 'step2' depends on non-existent step 'missing_step'",
    "Invalid cron expression in schedule: '* * * *'"
  ],
  "dag": {
    "name": "example-dag",
    // Partial DAG details when possible
  }
}

Notes:

  • Always returns 200 status - check valid field to determine if spec is valid
  • errors array contains human-readable validation messages
  • dag field may contain partial DAG details even when validation fails
  • Use this endpoint to validate DAG specs before creating or updating DAGs
  • warnings lists non-fatal spec warnings; they do not affect valid
  • issues lists the same errors, warnings, and notices as objects with severity (error, warning, or notice), source (load, build, warning, or valueReference), and message. An issue also has stepName, stepPath (such as steps[3] or handler_on.failure), fieldPath (such as steps[3].with.url), line, and column when known
  • valueReferenceNotices lists value-reference notices produced while loading the spec. They are not persisted
  • referenceScope lists what a step can reference: params, consts, env and secrets names (without their values), and steps with the outputs other steps can use. Each entry has its canonical reference token, such as ${params.region} or ${steps.fetch.outputs.body}. A step's outputsKnown is static when the spec gives its outputs and runtime when only a run does

Rename DAG ​

Endpoint: POST /api/v1/dags/{fileName}/rename

Changes the file ID of the DAG definition.

Request Body:

json
{
  "newFileName": "new-dag-name"
}

Response (200): Success

Error Response (400):

json
{
  "code": "bad_request",
  "message": "Invalid new file name format"
}

Error Response (409):

json
{
  "code": "already_exists",
  "message": "A DAG with the new name already exists"
}

DAG Execution Endpoints ​

Start DAG ​

Endpoint: POST /api/v1/dags/{fileName}/start

Creates and starts a DAG run with optional parameters.

Request Body:

json
{
  "params": "{\"env\": \"production\", \"version\": \"1.2.3\"}",
  "dagRunId": "custom-run-id",
  "dagName": "on-demand-data-load",
  "singleton": false,
  "noReuse": false
}

Request Fields:

FieldTypeDescriptionRequired
paramsstringJSON string of parameters. Values are validated and coerced against DAG param definitions or external parameter schema before execution. For named params, prefer a JSON object payload. JSON arrays are mainly for positional or mixed raw input.No
dagRunIdstringCustom run IDNo
dagNamestringOverride the DAG name used for this run (must satisfy DAG name validation)No
singletonbooleanIf true, prevent starting if DAG is already running (returns 409)No
noReusebooleanIf true, recompute eligible build steps instead of reusing prior materializationsNo
stepsstring[]Run only these steps, by step name or ID. Every other step is recorded as skipped and does not run. Not supported for agent DAGsNo
outputsFromRunIdstringFinished DAG-run of the same DAG whose work directory, and the outputs of the skipped steps that the selected steps depend on, are carried into the run. Only steps that succeeded there contribute outputs. Requires stepsNo
outputsFromRunsobjectThe run each skipped step takes its outputs from, keyed by step name or ID. Wins over outputsFromRunId for that step; the work directory still comes only from outputsFromRunId. Requires stepsNo
outputsobjectOutputs of skipped steps, keyed by step name or ID and then by output name, with string values. Wins over outputsFromRunId and outputsFromRuns. Requires stepsNo
outputsSourcesobjectThe run each step of outputs was captured from, keyed by step name or ID, as {"runId": "...", "capturedAt": "<RFC 3339>"}; capturedAt is optional. Requires stepsNo
ephemeralbooleanIf true, the run leaves no trace. See Ephemeral runs. Requires stepsNo

Tip: Set steps to try one step against the current definition without re-running the steps before it. The run is recorded like any other under the DAG's name. Only a skipped step that a selected step depends on, directly or through other steps, carries outputs. They come from outputsFromRunId, outputsFromRuns, or outputs; without them, references to those outputs stay unresolved. Retrying a failed selected-steps run follows normal retry rules, so steps downstream of the failed one run too.

The success, failure, abort, wait, and exit handlers of a selected-steps run run only when a selected step is a graph end step, one that no other step depends on. handler_on.init runs as usual. Retries of the run keep the same rule.

An outputsFromRuns entry must name a skipped step and a finished run of the same DAG in which that step succeeded. Otherwise the request returns 400 with the message step X cannot be reused from run Y: <reason>. The run details of the new run list the source of each carried step in reusedInputs.

For example, to run fetch with a token its upstream login step would normally publish:

json
{
  "steps": ["fetch"],
  "outputs": {"login": {"token": "abc123"}}
}

An outputs entry must name a skipped step. The name must be declared by the step's outputs, unless the step declares none, and a type: json output needs valid JSON. Naming the step's output: VAR sets that variable instead. Invalid entries return 400. The audit log records output names but not values.

outputsSources names the run each step's outputs came from, and the new run records it in reusedInputs. A step of outputs without an entry has no entry there. An entry for a step that is not in outputs returns 400. The named run does not need to exist.

Tip: Overriding the DAG name changes the identifier used for queue grouping, which is useful for ad-hoc executions that should not collide with scheduled runs.

Note: GET /api/v1/dags/{fileName} returns paramDefs for typed UI rendering. The start endpoint still accepts a JSON string in params, and the server performs the authoritative validation/coercion. For named params, clients should send a JSON object payload. Response (200):

json
{
  "dagRunId": "20240101_120000_abc123"
}

Response (409) - When singleton: true and DAG is already running:

json
{
  "code": "already_running",
  "message": "DAG example_dag is already running, cannot start in singleton mode"
}

Ephemeral runs ​

With ephemeral: true, the selected steps run in a run that leaves no trace. ephemeral: true without steps returns 400.

  • The run executes like any run, and its run details, step logs, step outputs, spec, and SSE stream are readable by its ID while it exists.
  • It is not listed in run lists, queues, the dashboard, artifacts, or the events feed. Get DAG Run History includes it only with includeEphemeral=true. It does not count toward history retention.
  • It produces no events, so notification rules and incident policies do not see it.
  • Retry, reschedule, and edit-retry requests for it return 409.
  • The server removes it 10 minutes after it ends, and removes every ended ephemeral run when the server starts. A run that has not ended is never removed.
  • The audit log records the start request, marked ephemeral.
  • Its run details and run summaries carry ephemeral: true.

An ephemeral run can be the outputsFromRunId or outputsFromRuns source of another request while it exists.

Start DAG (Synchronous) ​

Endpoint: POST /api/v1/dags/{fileName}/start-sync

Deprecated

Use POST /api/v1/dags/{fileName}/start for new integrations, then monitor the DAG run through the DAG-run status API or SSE. This endpoint remains available for backward compatibility.

Creates and starts a DAG run, waits for it to complete (or timeout), and returns the full execution details.

Request Body:

json
{
  "timeout": 300,
  "params": "{\"env\": \"production\"}",
  "dagRunId": "custom-run-id",
  "dagName": "sync-execution",
  "singleton": false,
  "noReuse": false
}

Request Fields:

FieldTypeDescriptionRequired
timeoutintegerMaximum seconds to wait for completion (1-86400)Yes
paramsstringJSON string of parameters. Values are validated and coerced against DAG param definitions or external parameter schema before execution.No
dagRunIdstringCustom run IDNo
dagNamestringOverride the DAG name used for this runNo
singletonbooleanIf true, prevent starting if DAG is already running (returns 409)No
noReusebooleanIf true, recompute eligible build steps instead of reusing prior materializationsNo

Response (200) - DAG completed or reached waiting status:

json
{
  "dagRun": {
    "rootDAGRunName": "my-dag",
    "rootDAGRunId": "20240101_120000_abc123",
    "dagRunId": "20240101_120000_abc123",
    "name": "my-dag",
    "status": 4,
    "statusLabel": "succeeded",
    "startedAt": "2024-01-01T12:00:00Z",
    "finishedAt": "2024-01-01T12:00:05Z",
    "log": "/logs/my-dag.log",
    "params": "{}",
    "nodes": [
      {
        "step": {"name": "step1", "command": "echo hello"},
        "status": 4,
        "statusLabel": "succeeded",
        "startedAt": "2024-01-01T12:00:01Z",
        "finishedAt": "2024-01-01T12:00:02Z"
      }
    ]
  }
}

Response (408) - Timeout waiting for completion (DAG continues in background):

json
{
  "code": "timeout",
  "message": "timeout waiting for DAG my-dag to complete after 300 seconds; DAG run continues in background",
  "dagRunId": "20240101_120000_abc123"
}

Note: When a timeout occurs, the DAG run continues executing in the background. The response includes the dagRunId so you can monitor or cancel the run using other API endpoints.

Response (409) - When singleton: true and DAG is already running:

json
{
  "code": "already_exists",
  "message": "DAG my-dag is already running (singleton mode)"
}

Note: If the DAG reaches a "waiting" status (e.g., a human-in-the-loop step requires approval), the endpoint returns immediately with 200 and the current status instead of blocking until timeout.

Enqueue DAG ​

Endpoint: POST /api/v1/dags/{fileName}/enqueue

Adds a DAG run to the queue for later execution.

Request Body:

json
{
  "params": "{\"key\": \"value\"}",
  "dagRunId": "optional-custom-id",
  "dagName": "queued-ad-hoc-run",
  "queue": "optional-queue-override",
  "noReuse": false
}

Request Fields:

FieldTypeDescriptionRequired
paramsstringJSON string of parameters. Values are validated and coerced against DAG param definitions or external parameter schema before the run is queued.No
dagRunIdstringCustom run IDNo
dagNamestringOverride the DAG name used for the queued run (must satisfy DAG name validation)No
queuestringQueue name overrideNo
noReusebooleanIf true, recompute eligible build steps when the queued run startsNo

Tip: When you override the DAG name, the queued run is tracked under the new identifier for both queue management and history records.

Response (200):

json
{
  "dagRunId": "20240101_120000_abc123"
}

Toggle DAG Suspension ​

Endpoint: POST /api/v1/dags/{fileName}/suspend

Controls whether the scheduler creates runs from this DAG.

Request Body:

json
{
  "suspend": true
}

Response (200): Success

Error Response (404):

json
{
  "code": "not_found",
  "message": "DAG not found"
}

DAG Run History Endpoints ​

Get DAG Run History ​

Endpoint: GET /api/v1/dags/{fileName}/dag-runs

Fetches execution history of a DAG.

Query Parameters:

ParameterTypeDescriptionDefault
includeEphemeralbooleanAlso return the ephemeral runs of this DAG, merged by start time under the same limit. Each carries ephemeral: true. No other list returns themfalse
remoteNodestringRemote node name"local"

Response (200):

json
{
  "dagRuns": [
    {
      "rootDAGRunName": "data_processing_pipeline",
      "rootDAGRunId": "20240211_140000_abc123",
      "parentDAGRunName": "",
      "parentDAGRunId": "",
      "dagRunId": "20240211_140000_abc123",
      "name": "data_processing_pipeline",
      "status": 4,
      "statusLabel": "succeeded",
      "queuedAt": "",
      "startedAt": "2024-02-11T14:00:00Z",
      "finishedAt": "2024-02-11T14:45:30Z",
      "log": "/logs/data_processing_pipeline/20240211_140000_abc123.log",
      "params": "{\"date\": \"2024-02-11\", \"env\": \"production\", \"batch_size\": 5000}"
    },
    {
      "rootDAGRunName": "data_processing_pipeline",
      "rootDAGRunId": "20240211_020000_def456",
      "parentDAGRunName": "",
      "parentDAGRunId": "",
      "dagRunId": "20240211_020000_def456",
      "name": "data_processing_pipeline",
      "status": 2,
      "statusLabel": "failed",
      "queuedAt": "",
      "startedAt": "2024-02-11T02:00:00Z",
      "finishedAt": "2024-02-11T02:15:45Z",
      "log": "/logs/data_processing_pipeline/20240211_020000_def456.log",
      "params": "{\"date\": \"2024-02-11\", \"env\": \"production\", \"batch_size\": 5000}"
    },
    {
      "rootDAGRunName": "data_processing_pipeline",
      "rootDAGRunId": "20240210_140000_ghi789",
      "parentDAGRunName": "",
      "parentDAGRunId": "",
      "dagRunId": "20240210_140000_ghi789",
      "name": "data_processing_pipeline",
      "status": 4,
      "statusLabel": "succeeded",
      "queuedAt": "",
      "startedAt": "2024-02-10T14:00:00Z",
      "finishedAt": "2024-02-10T14:42:15Z",
      "log": "/logs/data_processing_pipeline/20240210_140000_ghi789.log",
      "params": "{\"date\": \"2024-02-10\", \"env\": \"production\", \"batch_size\": 5000}"
    }
  ],
  "gridData": [
    {
      "name": "extract_data",
      "history": [4, 2, 4]
    },
    {
      "name": "transform_data",
      "history": [4, 2, 4]
    },
    {
      "name": "load_to_warehouse",
      "history": [4, 0, 4]
    }
  ]
}

Get Specific DAG Run ​

Endpoint: GET /api/v1/dags/{fileName}/dag-runs/{dagRunId}

Gets detailed status of a specific DAG run.

Response (200):

json
{
  "dagRun": {
    "rootDAGRunName": "data_processing_pipeline",
    "rootDAGRunId": "20240211_140000_abc123",
    "parentDAGRunName": "",
    "parentDAGRunId": "",
    "dagRunId": "20240211_140000_abc123",
    "name": "data_processing_pipeline",
    "status": 4,
    "statusLabel": "succeeded",
    "queuedAt": "",
    "startedAt": "2024-02-11T14:00:00Z",
    "finishedAt": "2024-02-11T14:45:30Z",
    "log": "/logs/data_processing_pipeline/20240211_140000_abc123.log",
    "params": "{\"date\": \"2024-02-11\", \"env\": \"production\", \"batch_size\": 5000}",
    "nodes": [
      {
        "step": {
          "name": "extract_data",
          "id": "extract",
          "command": "python",
          "args": ["extract.py", "--date", "2024-02-11"]
        },
        "stdout": "/logs/data_processing_pipeline/20240211_140000_abc123/extract_data.stdout",
        "stderr": "/logs/data_processing_pipeline/20240211_140000_abc123/extract_data.stderr",
        "startedAt": "2024-02-11T14:00:30Z",
        "finishedAt": "2024-02-11T14:15:45Z",
        "status": 4,
        "statusLabel": "succeeded",
        "retryCount": 0,
        "doneCount": 1,
        "subRuns": [],
        "subRunsRepeated": [],
        "error": ""
      },
      {
        "step": {
          "name": "transform_data",
          "id": "transform",
          "command": "python transform.py --input=/tmp/extracted_20240211.csv",
          "depends": ["extract_data"]
        },
        "stdout": "/logs/data_processing_pipeline/20240211_140000_abc123/transform_data.stdout",
        "stderr": "/logs/data_processing_pipeline/20240211_140000_abc123/transform_data.stderr",
        "startedAt": "2024-02-11T14:15:45Z",
        "finishedAt": "2024-02-11T14:30:20Z",
        "status": 4,
        "statusLabel": "succeeded",
        "retryCount": 0,
        "doneCount": 1,
        "subRuns": [],
        "subRunsRepeated": [],
        "error": ""
      },
      {
        "step": {
          "name": "load_to_warehouse",
          "id": "load",
          "run": "warehouse-loader",
          "params": "{\"file\": \"/tmp/transformed_20240211.csv\", \"table\": \"fact_sales\"}",
          "depends": ["transform_data"]
        },
        "stdout": "/logs/data_processing_pipeline/20240211_140000_abc123/load_to_warehouse.stdout",
        "stderr": "/logs/data_processing_pipeline/20240211_140000_abc123/load_to_warehouse.stderr",
        "startedAt": "2024-02-11T14:30:20Z",
        "finishedAt": "2024-02-11T14:45:30Z",
        "status": 4,
        "statusLabel": "succeeded",
        "retryCount": 0,
        "doneCount": 1,
        "subRuns": [
          {
            "dagRunId": "sub_20240211_143020_xyz456",
            "params": "{\"file\": \"/tmp/transformed_20240211.csv\", \"table\": \"fact_sales\"}"
          }
        ],
        "subRunsRepeated": [],
        "error": ""
      }
    ]
  }
}

DAG Run Management Endpoints ​

List All DAG Runs ​

Endpoint: GET /api/v1/dag-runs

Retrieves the newest DAG runs with forward-only cursor pagination.

Query Parameters:

ParameterTypeDescriptionDefault
namestringFilter by DAG name-
statusintegerFilter by status (0-8)-
fromDateintegerUnix timestamp start-
toDateintegerUnix timestamp end-
dagRunIdstringFilter by run ID-
limitintegerPage size (max 500)100
cursorstringOpaque cursor returned by the previous page-
labelsstringFilter by DAG labels. Comma-separated. Same syntax as DAG filtering. Supports * and ? wildcards. AND logic.-
remoteNodestringRemote node name"local"

The response includes nextCursor when older results are available. Repeat the same request with cursor=<nextCursor> to continue.

Status Values:

  • 0: Not started
  • 1: Running
  • 2: Failed
  • 3: Aborted
  • 4: Success
  • 5: Queued
  • 6: Partial Success
  • 7: Waiting
  • 8: Rejected

DAG-run Runtime Conditions ​

DAGRunSummary and DAGRunDetails may include a conditions array when the DAG run status is queued. The current implementation emits queued runtime conditions only while the run remains queued. Conditions are omitted for non-queued statuses, and invalid checkedAt values are skipped when the API response is built.

Each condition object has:

FieldTypeDescription
typestringCondition type. The current queued condition type is Queued.
statusstringTrue, False, or Unknown by schema. The scheduler currently writes True for queued conditions.
reasonstringMachine-readable reason.
messagestringHuman-readable explanation.
checkedAtstringRFC 3339 timestamp when the scheduler observed the condition.

Current queued condition reasons:

ReasonMeaning
QueueConcurrencyLimitReachedThe queue's active-run concurrency limit currently has no free slot.
DispatchAdmissionPendingA distributed dispatch reservation already exists for the run.
WorkerSelectorNotMatchedNo healthy worker matches the run's required worker_selector.
NoAvailableWorkerNo healthy distributed worker is available.
DispatchUnavailableDistributed dispatch is temporarily unavailable and the error was not classified as a worker selector mismatch or no available worker.

Response (200):

json
{
  "dagRuns": [
    {
      "rootDAGRunName": "data_processing_pipeline",
      "rootDAGRunId": "20240211_160000_current",
      "parentDAGRunName": "",
      "parentDAGRunId": "",
      "dagRunId": "20240211_160000_current",
      "name": "data_processing_pipeline",
      "status": 1,
      "statusLabel": "running",
      "queuedAt": "",
      "startedAt": "2024-02-11T16:00:00Z",
      "finishedAt": "",
      "log": "/logs/data_processing_pipeline/20240211_160000_current.log",
      "params": "{\"date\": \"2024-02-11\", \"env\": \"production\", \"batch_size\": 5000}"
    },
    {
      "rootDAGRunName": "database_backup",
      "rootDAGRunId": "20240211_150000_backup",
      "parentDAGRunName": "",
      "parentDAGRunId": "",
      "dagRunId": "20240211_150000_backup",
      "name": "database_backup",
      "status": 4,
      "statusLabel": "succeeded",
      "queuedAt": "",
      "startedAt": "2024-02-11T15:00:00Z",
      "finishedAt": "2024-02-11T15:45:30Z",
      "log": "/logs/database_backup/20240211_150000_backup.log",
      "params": "{\"target_db\": \"production\", \"retention_days\": 30}"
    },
    {
      "rootDAGRunName": "ml_training_pipeline",
      "rootDAGRunId": "20240211_143000_ml",
      "parentDAGRunName": "",
      "parentDAGRunId": "",
      "dagRunId": "20240211_143000_ml",
      "name": "ml_training_pipeline",
      "status": 5,
      "statusLabel": "queued",
      "queuedAt": "2024-02-11T14:30:00Z",
      "startedAt": "",
      "finishedAt": "",
      "log": "/logs/ml_training_pipeline/20240211_143000_ml.log",
      "conditions": [
        {
          "type": "Queued",
          "status": "True",
          "reason": "WorkerSelectorNotMatched",
          "message": "No healthy worker matches the required worker selector; DAG-run is waiting in the queue.",
          "checkedAt": "2024-02-11T14:31:00Z"
        }
      ],
      "params": "{\"model\": \"recommendation_v2\", \"dataset\": \"user_interactions\"}"
    }
  ],
  "nextCursor": "eyJvY2N1cnJlZEF0IjoiMjAyNC0wMi0xMVQxNDozMDowMFoiLCJkYWdSdW5JZCI6IjIwMjQwMjExXzE0MzAwMF9tbCJ9"
}

Execute DAG Run from Inline Spec ​

Endpoint: POST /api/v1/dag-runs

Creates and starts a DAG-run directly from an inline YAML specification without writing a DAG file to disk.

Inline specifications have no source directory, so they cannot supply file dependencies for distributed execution. Save the workflow as a DAG file and start the saved DAG when a worker needs files stored beside it.

Query Parameters:

ParameterTypeDescriptionDefault
remoteNodestringTarget remote node for execution"local"

Request Body:

json
{
  "spec": "steps:\n  - name: extract\n    run: ./extract.sh",
  "name": "ad-hoc-extract",
  "params": "{\"env\":\"prod\"}",
  "dagRunId": "20241101_010203_custom",
  "singleton": true,
  "noReuse": false
}

Request Fields:

FieldTypeDescriptionRequired
specstringDAG specification in YAML formatYes
namestringOverride DAG name used for validation and executionNo
paramsstringJSON string persisted with the DAG-run and exposed to stepsNo
dagRunIdstringExplicit run identifier. If omitted, one is generatedNo
singletonbooleanWhen true, aborts with 409 if the DAG already has active or queued runsNo
noReusebooleanIf true, recompute eligible build steps instead of reusing prior materializationsNo
stepsstring[]Run only these steps, by step name or ID; every other step is recorded as skipped. See Start DAGNo
outputsFromRunIdstringFinished DAG-run of the same DAG whose step outputs feed the skipped steps. Requires stepsNo
outputsFromRunsobjectThe run each skipped step takes its outputs from, keyed by step. Requires stepsNo
outputsobjectOutputs of skipped steps, keyed by step and then output name. Requires stepsNo
outputsSourcesobjectThe run each step of outputs was captured from. Requires stepsNo
ephemeralbooleanRun the selected steps in a run that leaves no trace. See Ephemeral runs. Requires stepsNo

Response (200):

json
{
  "dagRunId": "20241101_010203_custom"
}

Error Response (400) - Invalid spec or missing spec field:

json
{
  "code": "bad_request",
  "message": "spec is required"
}

Error Response (409) - Singleton guard or queue concurrency limit blocking execution:

json
{
  "code": "max_run_reached",
  "message": "DAG ad-hoc-extract is already running, cannot start"
}

Example:

bash
curl -X POST "http://localhost:8080/api/v1/dag-runs?remoteNode=local" \
  -H "Content-Type: application/json" \
  -d '{
        "spec": "steps:\n  - name: echo\n    run: echo \"hello\"",
        "name": "adhoc-hello",
        "params": "{\"requestedBy\":\"cli\"}"
      }'

Enqueue DAG Run from Inline Spec ​

Endpoint: POST /api/v1/dag-runs/enqueue

Queues a DAG-run from an inline YAML spec without persisting a DAG file. The run follows queue semantics (global queue concurrency limits, queue overrides) and is started by the scheduler.

Inline specifications have no source directory, so they cannot supply file dependencies for distributed execution. Save the workflow as a DAG file and enqueue the saved DAG when a worker needs files stored beside it.

Query Parameters:

ParameterTypeDescriptionDefault
remoteNodestringTarget remote node for queuing"local"

Request Body:

json
{
  "spec": "steps:\n  - name: sleep\n    run: sleep 60",
  "name": "queued-sleeper",
  "params": "{\"duration\":60}",
  "dagRunId": "queue_20241101_010203",
  "queue": "low-priority",
  "noReuse": false
}

Request Fields:

FieldTypeDescriptionRequired
specstringDAG specification in YAML formatYes
namestringOverride DAG name if the spec omits oneNo
paramsstringJSON string saved with the queued runNo
dagRunIdstringExplicit run ID. When omitted, one is generatedNo
queuestringQueue name override for this run onlyNo
noReusebooleanIf true, recompute eligible build steps when the queued run startsNo

Response (200):

json
{
  "dagRunId": "queue_20241101_010203"
}

Error Response (409) - DAG already queued or queue concurrency limit reached:

json
{
  "code": "max_run_reached",
  "message": "DAG queued-sleeper is already in the queue, cannot enqueue"
}

Example:

bash
curl -X POST "http://localhost:8080/api/v1/dag-runs/enqueue" \
  -H "Content-Type: application/json" \
  -d '{
        "spec": "steps:\n  - name: notify\n    run: ./notify.sh",
        "name": "queued-notify",
        "queue": "alerts"
      }'

Get DAG Run Details ​

Endpoint: GET /api/v1/dag-runs/{name}/{dagRunId}

Fetches detailed status of a specific DAG run. You can use the special value "latest" as the dagRunId to retrieve the most recent DAG run for the specified DAG.

Examples:

  • GET /api/v1/dag-runs/data-pipeline/20240211_120000 - Get a specific run
  • GET /api/v1/dag-runs/data-pipeline/latest - Get the latest run

Path Parameters:

ParameterTypeDescription
namestringDAG name
dagRunIdstringDAG run ID or "latest"

When the returned DAG run is still queued, dagRun.conditions can contain queued runtime conditions using the shape described in DAG-run Runtime Conditions. Non-queued DAG runs omit conditions.

Response (200):

json
{
  "dagRun": {
    "dagRunId": "20240211_120000",
    "name": "data-pipeline",
    "status": 4,
    "statusLabel": "succeeded",
    "startedAt": "2024-02-11T12:00:00Z",
    "finishedAt": "2024-02-11T12:15:00Z",
    "params": "{\"date\": \"2024-02-11\", \"env\": \"prod\"}",
    "nodes": [
      {
        "step": {
          "name": "extract",
          "command": "python extract.py"
        },
        "status": 4,
        "statusLabel": "succeeded",
        "startedAt": "2024-02-11T12:00:00Z",
        "finishedAt": "2024-02-11T12:05:00Z",
        "retryCount": 0,
        "stdout": "/logs/data-pipeline/20240211_120000/extract.out",
        "stderr": "/logs/data-pipeline/20240211_120000/extract.err"
      },
      {
        "step": {
          "name": "transform",
          "command": "python transform.py",
          "depends": ["extract"]
        },
        "status": 4,
        "statusLabel": "succeeded",
        "startedAt": "2024-02-11T12:05:00Z",
        "finishedAt": "2024-02-11T12:10:00Z",
        "retryCount": 0
      },
      {
        "step": {
          "name": "load",
          "run": "sub-workflow",
          "params": "TARGET=warehouse"
        },
        "status": 4,
        "statusLabel": "succeeded",
        "startedAt": "2024-02-11T12:10:00Z",
        "finishedAt": "2024-02-11T12:15:00Z",
        "subRuns": [
          {
            "dagRunId": "sub_20240211_121000",
            "name": "sub-workflow",
            "status": 4,
            "statusLabel": "succeeded"
          }
        ]
      }
    ]
  }
}

startedAt and finishedAt of runs, steps, and sub DAG-runs are RFC 3339 timestamps with millisecond precision, such as 2024-02-11T12:00:00.158Z. A run that an older version stored has second precision.

A node whose step repeats with a repeat mode and no limit carries continuous: true. Its executions are recorded as samples; see Continuous Step Endpoints.

Build Execution Data ​

For type: build workflows, the DAG-run object includes noReuse, and each node includes a build decision:

json
{
  "dagRun": {
    "noReuse": false,
    "nodes": [
      {
        "statusLabel": "succeeded",
        "build": {
          "decision": "reuse",
          "phase": "complete",
          "reason": "matched",
          "detail": "recipe, inputs, and output match the committed manifest",
          "fingerprint": "sha256:...",
          "materializationKey": "...",
          "producerRun": {
            "name": "report-pipeline",
            "id": "previous-run-id"
          },
          "producerAttemptId": "previous-attempt-id"
        }
      }
    ]
  }
}

decision is one of none, always, execute, reuse, or deferred. phase is one of precondition, evaluate, execute, verify, commit, or complete. reason is stable for programmatic handling, while detail is the user-facing explanation. Producer fields are present for reused materializations.

See Build Workflows for the decision rules.

Reused Inputs ​

A selected-steps run lists where the carried outputs of its skipped steps came from in dagRun.reusedInputs, one entry per skipped step, in DAG step order. The field is absent when the run carried no outputs.

json
{
  "dagRun": {
    "reusedInputs": [
      {
        "step": "test",
        "runId": "20241101_080000",
        "capturedAt": "2024-11-01T08:00:12.480Z",
        "startedAt": "2024-11-01T08:00:10.105Z",
        "finishedAt": "2024-11-01T08:00:12.480Z",
        "via": "run"
      }
    ]
  }
}

via is run when the outputs came from outputsFromRunId or outputsFromRuns. startedAt and finishedAt are then the step's times in that run, and capturedAt is its finish time. via is cache when the outputs came from outputs with an outputsSources entry. runId and capturedAt then come from that entry, and the step times are absent. The record stays on the run for all its attempts.

Model Usage ​

A node whose step called models carries modelUsage.models: one entry per provider and model, over every attempt of the step, including what a coding agent the step ran reported. modelUsage.notReported names a tool whose usage could not be read. dagRun.modelUsage.models totals every step of the run, handlers included.

json
{
  "provider": "openai",
  "model": "gpt-5",
  "calls": 3,
  "inputTokens": 5210,
  "cachedInputTokens": 4096,
  "cacheWriteTokens": 0,
  "outputTokens": 812,
  "reasoningTokens": 256,
  "cost": 0.0123,
  "costReportedCalls": 3
}

inputTokens includes cached and cache-write tokens, and outputTokens includes reasoning tokens. cost is in US dollars, as the provider reported it for costReportedCalls of the calls. It covers every call only when costReportedCalls equals calls.

The body steps of a foreach item, as returned by GET /api/v1/dag-runs/{name}/{dagRunId}/steps/{stepName}/foreach/{item}, carry modelUsage and, when they kept one, such as the operations of a browser step, agentSession. Both have the shape that the run's own nodes have, with the run's secrets masked.

Stop or Cancel DAG Run ​

Endpoint: POST /api/v1/dag-runs/{name}/{dagRunId}/stop

Forcefully stops a running DAG run, or cancels a failed root DAG run that is still pending DAG-level automatic retry.

Response (200): Success

Error Response (404):

json
{
  "code": "not_found",
  "message": "DAG run not found"
}

Error Response (400):

json
{
  "code": "not_running",
  "message": "DAG is not currently running"
}

Stop All DAG Runs ​

Endpoint: POST /api/v1/dags/{fileName}/stop-all

Forcefully stops all currently running instances of a DAG. This is useful when multiple instances of the same DAG are running simultaneously.

Response (200):

json
{
  "errors": []
}

Error Response (404):

json
{
  "code": "not_found",
  "message": "DAG not found"
}

Retry DAG Run ​

Endpoint: POST /api/v1/dag-runs/{name}/{dagRunId}/retry

Creates a new DAG run based on a previous execution.

Request Body:

json
{
  "dagRunId": "new-run-id"
}

Response (200): Success

Error Response (404):

json
{
  "code": "not_found",
  "message": "Original DAG run not found"
}

Error Response (400):

json
{
  "code": "already_running",
  "message": "Another instance of this DAG is already running"
}

Error Response (409) - The DAG run is ephemeral.

Reschedule DAG Run ​

Endpoint: POST /api/v1/dag-runs/{name}/{dagRunId}/reschedule

Launches a fresh DAG run by reusing the captured parameters from a historic execution. Useful for re-running successful or failed runs while keeping the original parameter set.

Request Body:

json
{
  "dagRunId": "optional-new-id",
  "dagName": "optional-name-override"
}

Request Fields:

FieldTypeDescriptionRequired
dagRunIdstringCustom ID for the new run. If omitted a unique ID is generated.No
dagNamestringOverride the DAG name used for the rescheduled run. Must pass DAG name validation.No

Rescheduled runs always respect singleton mode. When the target DAG already has active or queued runs, the request is rejected.

Response (200):

json
{
  "dagRunId": "20241101_010203_xyz",
  "queued": false
}

Error Response (409) - When the DAG already has active or queued runs, or the historic run is ephemeral:

json
{
  "code": "max_run_reached",
  "message": "DAG example_dag is already running, cannot start"
}

Error Response (404):

json
{
  "code": "not_found",
  "message": "DAG run not found"
}

Dequeue DAG Run ​

Endpoint: GET /api/v1/dag-runs/{name}/{dagRunId}/dequeue

Removes a queued DAG run from the queue.

Response (200): Success

Error Response (404):

json
{
  "code": "not_found",
  "message": "DAG run not found in queue"
}

Log Endpoints ​

Get DAG Run Log ​

Endpoint: GET /api/v1/dag-runs/{name}/{dagRunId}/log

Fetches the execution log for a DAG run.

Query Parameters:

ParameterTypeDescriptionDefault
tailintegerLines from end-
headintegerLines from start-
offsetintegerStart line (1-based)-
limitintegerMax lines (max 10000)-
remoteNodestringRemote node name"local"

Response (200):

json
{
  "content": "2024-02-12 14:00:00 INFO DAG data_processing_pipeline started\n2024-02-12 14:00:00 INFO Run ID: 20240212_140000_abc123\n2024-02-12 14:00:00 INFO Parameters: {\"date\": \"2024-02-12\", \"env\": \"production\", \"batch_size\": 5000}\n2024-02-12 14:00:00 INFO Checking preconditions...\n2024-02-12 14:00:01 INFO Precondition passed: Weekday check (current day: 1)\n2024-02-12 14:00:01 INFO Starting step: extract_data\n2024-02-12 14:00:30 INFO [extract_data] Executing: python extract.py --date 2024-02-12\n2024-02-12 14:15:45 INFO [extract_data] Step completed successfully\n2024-02-12 14:15:45 INFO [extract_data] Output saved to variable: EXTRACTED_FILE = /tmp/extracted_20240212.csv\n2024-02-12 14:15:45 INFO Starting step: transform_data\n2024-02-12 14:15:45 INFO [transform_data] Executing: python transform.py --input=/tmp/extracted_20240212.csv\n2024-02-12 14:30:20 INFO [transform_data] Step completed successfully\n2024-02-12 14:30:20 INFO [transform_data] Output saved to variable: TRANSFORMED_FILE = /tmp/transformed_20240212.csv\n2024-02-12 14:30:20 INFO Starting step: load_to_warehouse\n2024-02-12 14:30:20 INFO [load_to_warehouse] Running sub DAG: warehouse-loader\n2024-02-12 14:30:20 INFO [load_to_warehouse] Sub DAG started with ID: sub_20240212_143020_xyz456\n2024-02-12 14:45:30 INFO [load_to_warehouse] Sub DAG completed successfully\n2024-02-12 14:45:30 INFO Executing onSuccess handler: notify_success\n2024-02-12 14:45:32 INFO [notify_success] Handler completed\n2024-02-12 14:45:32 INFO Executing onExit handler: cleanup\n2024-02-12 14:45:35 INFO [cleanup] Handler completed\n2024-02-12 14:45:35 INFO DAG completed successfully\n",
  "lineCount": 22,
  "totalLines": 156,
  "hasMore": true,
  "isEstimate": false
}

Get Step Log ​

Endpoint: GET /api/v1/dag-runs/{name}/{dagRunId}/steps/{stepName}/log

Fetches the log for a specific step.

Query Parameters:

ParameterTypeDescriptionDefault
streamstring"stdout" or "stderr""stdout"
tailintegerLines from end-
headintegerLines from start-
offsetintegerStart line (1-based)-
limitintegerMax lines (max 10000)-

Response (200):

json
{
  "content": "2024-02-11 12:05:00 INFO Starting data transformation...\n2024-02-11 12:05:01 INFO Processing 1000 records\n2024-02-11 12:05:05 INFO Transformation complete\n",
  "lineCount": 3,
  "totalLines": 3,
  "hasMore": false,
  "isEstimate": false
}

Response with stderr (200):

json
{
  "content": "2024-02-11 12:05:02 WARNING Duplicate key found, skipping record ID: 123\n2024-02-11 12:05:03 WARNING Invalid date format in record ID: 456\n",
  "lineCount": 2,
  "totalLines": 2,
  "hasMore": false,
  "isEstimate": false
}

DAG Run Outputs Endpoint ​

Get DAG Run Outputs ​

Endpoint: GET /api/v1/dag-runs/{name}/{dagRunId}/outputs

Retrieves the collected outputs from a completed DAG run. This endpoint reads the run's outputs.json, which is populated by string-form output: NAME, stdout.outputs, and action: outputs.write.

Path Parameters:

ParameterTypeDescription
namestringDAG name
dagRunIdstringDAG run ID

Query Parameters:

ParameterTypeDescriptionDefault
remoteNodestringRemote node name"local"

Response (200):

json
{
  "metadata": {
    "dagName": "data-processing-pipeline",
    "dagRunId": "20240211_140000_abc123",
    "attemptId": "attempt_20240211_140000_001",
    "status": "succeeded",
    "completedAt": "2024-02-11T14:45:30Z",
    "params": "{\"date\": \"2024-02-11\", \"env\": \"production\"}"
  },
  "outputs": {
    "extractedFile": "/tmp/extracted_20240211.csv",
    "recordCount": "50000",
    "transformedFile": "/tmp/transformed_20240211.csv"
  }
}

Response Fields:

  • metadata: Execution context for the outputs
    • dagName: Name of the executed DAG
    • dagRunId: Unique identifier for the DAG run
    • attemptId: Identifier for the specific execution attempt
    • status: Final status of the DAG run ("succeeded", "partially_succeeded", "failed", "rejected", "aborted")
    • completedAt: RFC3339 timestamp when execution completed
    • params: JSON-serialized parameters passed to the DAG
  • outputs: Key-value pairs of collected outputs
    • String-form output: NAME, stdout.outputs, and outputs.write values appear here
    • String-form output: NAME keys are converted from SCREAMING_SNAKE_CASE to camelCase; stdout.outputs and outputs.write keys are preserved
    • Object-form output: {...} values are step-scoped only and are not returned by this endpoint

Response when no outputs (200):

json
{
  "metadata": {
    "dagName": "",
    "dagRunId": "",
    "attemptId": "",
    "status": "",
    "completedAt": "0001-01-01T00:00:00Z"
  },
  "outputs": {}
}

Error Response (404):

json
{
  "code": "not_found",
  "message": "dag-run ID 20240211_140000_abc123 not found for DAG data-processing-pipeline"
}

Notes:

  • Outputs are only available for completed DAG runs (succeeded, partially_succeeded, failed, rejected, or aborted)
  • Secret values in outputs are automatically masked with *******
  • Object-form output: {...} does not participate in run-level outputs unless values are republished through stdout.outputs or outputs.write

Example:

bash
# Get outputs from a specific DAG run
curl "http://localhost:8080/api/v1/dag-runs/data-processing-pipeline/20240211_140000_abc123/outputs" \
     -H "Authorization: Bearer your-token"

# Get outputs from the latest DAG run
curl "http://localhost:8080/api/v1/dag-runs/data-processing-pipeline/latest/outputs" \
     -H "Authorization: Bearer your-token"

Get Step Outputs ​

Endpoint: GET /api/v1/dag-runs/{name}/{dagRunId}/steps/{stepName}/outputs

Returns the persisted outputs of one step. stepName is a step name or ID, a handler step's name or ID, or a handler key such as handler_on.exit.

Response (200):

json
{
  "stepName": "fetch_data",
  "stepId": "fetch",
  "status": 4,
  "statusLabel": "succeeded",
  "outputs": [
    {"name": "body", "value": "{\"count\":42}", "truncated": false, "size": 12}
  ],
  "outputVariable": {"name": "RESULT", "value": "ok", "truncated": false, "size": 2}
}
  • outputs holds the step's declared outputs, sorted by name, as ${steps.<id>.outputs.<name>} resolves them. For a step that declares none, it holds the published values.
  • outputVariable is the value of the step's string-form output: NAME, when it has one.
  • Each value is cut at 65536 bytes. truncated is true when it was cut, and size is its length in bytes before the cut.
  • Secret values are masked before they are persisted.
  • Returns 404 when the DAG run or the step does not exist.

Continuous Step Endpoints ​

A continuous step is a step whose repeat_policy has a repeat mode and no limit. Its node in the run details carries continuous: true. Instead of keeping every execution in the run status, Dagu records one sample per execution with its start time, duration, and exit code. A sample with a non-zero exit code is a failed sample. These endpoints read the samples of one step. They accept the optional remoteNode query parameter.

MethodEndpointDescription
GET/api/v1/dag-runs/{name}/{dagRunId}/steps/{stepName}/samplesSamples summarized in time buckets
GET/api/v1/dag-runs/{name}/{dagRunId}/steps/{stepName}/eventsStarts, stops, failures, and recoveries, newest first
GET/api/v1/dag-runs/{name}/{dagRunId}/steps/{stepName}/availabilityShare of samples that succeeded over the last 24 hours, 7 days, 30 days, and 365 days

A request for a run or step that does not exist, or for a step that is not continuous, returns 404. A query parameter out of its range, from after to, or a before cursor that the server did not give returns 400.

Get Step Samples ​

Query Parameters:

ParameterTypeDescriptionDefault
fromstringStart of the window (RFC 3339)24 hours before to
tostringEnd of the window (RFC 3339)now
pointsintegerMost buckets to return (10-2000)600

The server keeps samples in four tiers: 1-second buckets for 24 hours, 1-minute buckets for 7 days, 1-hour buckets for 30 days, and 24-hour buckets with no limit. It answers from the finest tier whose retention covers from and whose bucket count in the window is at most points.

Response (200):

json
{
  "resolutionSeconds": 60,
  "from": "2026-10-31T00:00:00Z",
  "to": "2026-11-01T00:00:00Z",
  "buckets": [
    {"time": "2026-10-31T00:00:00Z", "count": 60, "failures": 1, "minMs": 12, "maxMs": 380, "p50Ms": 18, "p95Ms": 44}
  ]
}

resolutionSeconds is 1, 60, 3600, or 86400. Buckets are in time order and start in UTC. A bucket with no sample is not listed.

Get Step Events ​

Query Parameters:

ParameterTypeDescriptionDefault
fromstringStart of the window (RFC 3339), inclusiveno bound
tostringEnd of the window (RFC 3339), exclusiveno bound
limitintegerMost events to return (1-500)100
beforestringnextCursor from a previous response-

Response (200):

json
{
  "events": [
    {"time": "2026-11-01T09:12:05.120Z", "kind": "recovered", "exitCode": 0, "failedCount": 42, "outageSeconds": 42},
    {"time": "2026-11-01T09:11:23.004Z", "kind": "failed", "exitCode": 7, "message": "connection refused"}
  ],
  "nextCursor": "..."
}

kind is started, stopped, failed, or recovered. message is the last line of a failed execution's standard error, 256 bytes at most. failedCount counts the failed samples of the outage, and outageSeconds is its length. A failed event is also recorded after each hour of an outage. nextCursor is absent when there are no more events.

Get Step Availability ​

Response (200):

json
{
  "windows": [
    {"window": "24h", "count": 86400, "failures": 42, "availability": 0.99951},
    {"window": "7d", "count": 604800, "failures": 42, "availability": 0.99993},
    {"window": "30d", "count": 2592000, "failures": 310, "availability": 0.99988},
    {"window": "365d", "count": 2592000, "failures": 310, "availability": 0.99988}
  ],
  "streak": 3175,
  "lastFailureAt": "2026-11-01T09:12:04.950Z",
  "totalCount": 2592000
}

availability is the share of samples that succeeded, from 0 to 1, and is absent when count is 0. streak counts the samples since the last failed sample, and lastFailureAt is that sample's time; it is absent when no sample failed. Once the 1-second tier no longer holds that sample, lastFailureAt is the start of the bucket that holds it, and streak is a lower bound. totalCount counts every execution.

See Repeat Without a Limit for continuous steps.

Step Management Endpoints ​

Update Step Status ​

Endpoint: PATCH /api/v1/dag-runs/{name}/{dagRunId}/steps/{stepName}/status

Manually updates a step's execution status.

Request Body:

json
{
  "status": 4
}

Status Values:

  • 0: Not started
  • 1: Running
  • 2: Failed
  • 3: Aborted
  • 4: Success
  • 5: Skipped
  • 6: Partial Success
  • 7: Waiting
  • 8: Rejected
  • 9: Retrying

Response (200): Success

Error Response (404):

json
{
  "code": "not_found",
  "message": "Step not found in DAG run"
}

Error Response (400):

json
{
  "code": "bad_request",
  "message": "Invalid status value"
}

Human Task Endpoints ​

These endpoints complete or push back standalone action: human.task steps. They operate on root DAG runs regardless of whether the run executes locally or on a distributed worker. The caller needs permission to execute DAGs in the run's workspace.

After completion, DAG-run node responses expose the actor as humanTaskCompletedBy and humanTaskCompletedById. These fields may be absent on older runs or when authentication is disabled.

Complete Human Task ​

Endpoint: POST /api/v1/dag-runs/{name}/{dagRunId}/human-tasks/{stepId}/complete

Validates typed input against the form stored with the run, then durably completes the human-task step. The request body is the form input object itself, not an inputs wrapper.

Request Body:

json
{
  "environment": "production",
  "replicas": 3,
  "notify": true
}

Send {} for an acknowledgement-only task. The request body is limited to 16 MiB.

Response (200):

json
{
  "dagName": "release-workflow",
  "dagRunId": "20260720_120000",
  "stepId": "review",
  "alreadyCompleted": false,
  "queued": true,
  "remainingWaitingSteps": 0,
  "resumeRequested": true
}
FieldTypeDescription
dagNamestringName of the root DAG.
dagRunIdstringConcrete DAG-run ID.
stepIdstringExplicit ID of the completed human-task step.
alreadyCompletedbooleanWhether identical input had already completed this task.
queuedbooleanWhether this request durably added the DAG-run retry to the queue.
remainingWaitingStepsintegerNumber of manual steps that still need input.
resumeRequestedbooleanWhether this request found the DAG run waiting and ready to resume, whether it queued the resume or a concurrent request queued it first. false when the run keeps waiting or has already left Waiting.

Dagu enqueues a retry of the same DAG run when the completion unblocks a step or no manual steps remain waiting; completion never starts the DAG immediately. A step counts as unblocked when every dependency lets it run, it declares no build inputs, and no step in the run has failed or been aborted. Otherwise Dagu stores the completion and returns queued: false and resumeRequested: false. While a resumed attempt is queued or running, completing another open task returns 409. A repeated request with the same canonical input is idempotent. Different input for an already completed task returns a conflict.

Error Responses:

  • 400: The body is not a JSON object or does not satisfy the stored form.
  • 404: The DAG run or human-task step does not exist or is not visible to the caller.
  • 409: The task is not actionable, the run changed concurrently, or prior input conflicts.
  • 413: The request body exceeds 16 MiB.
  • 503: The completion was stored, but the DAG-run retry could not be queued.

A 503 with error code human_task_resume_failed includes completionStored: true and resumePending: true in details. Do not ask the operator to re-enter the form. Retry only the queue operation with the endpoint below.

Retry Human Task Resume Queue ​

Endpoint: POST /api/v1/dag-runs/{name}/{dagRunId}/human-tasks/resume

Queues a recoverable human-task checkpoint after a completion or push-back was stored but the original enqueue attempt failed. This endpoint has no request body and never needs the previously submitted form values or feedback.

Response (200):

json
{
  "dagName": "release-workflow",
  "dagRunId": "20260720_120000",
  "queued": true
}

The operation is safe to retry. queued is false when the retry is already queued or the run has already resumed.

Error Responses:

  • 404: The DAG run does not exist or is not visible to the caller.
  • 409: Manual steps are still waiting and no step is ready to run, or the run has no completed human-task checkpoint or stored push-back to resume.
  • 503: The queue attempt failed again and remains retryable.

Push Back Human Task ​

Endpoint: POST /api/v1/dag-runs/{name}/{dagRunId}/human-tasks/{stepId}/push-back

Sends a waiting human task that declares with.push_back back to its rewind target. Dagu validates the feedback against the stored push-back form, then atomically resets the rewind target and every step that depends on it, including the task, with the feedback as push-back context. The request body is the feedback object itself, not an inputs wrapper.

Query Parameters:

ParameterTypeDescription
expectedIterationintegerOptional. Push-back iteration of the task the caller reviewed, 0 before the first push-back. A different current iteration returns 409 without changes.

Request Body:

json
{
  "feedback": "Add coverage for the empty input case"
}

Send {} when the task declares no push-back form. The request body is limited to 16 MiB.

Response (200):

json
{
  "dagName": "review-loop",
  "dagRunId": "20260720_120000",
  "stepId": "review",
  "rewindTo": "implement",
  "iteration": 1,
  "alreadyPushedBack": false,
  "queued": true,
  "resumeRequested": true
}
FieldTypeDescription
dagNamestringName of the root DAG.
dagRunIdstringConcrete DAG-run ID.
stepIdstringExplicit ID of the pushed-back human-task step.
rewindTostringName of the step that runs again first.
iterationintegerPush-back iteration the push-back recorded.
alreadyPushedBackbooleanWhether identical feedback had already pushed the task back and the task has not opened again.
queuedbooleanWhether this request durably added the DAG-run retry to the queue.
resumeRequestedbooleanWhether the run was ready to resume, whether this request or a concurrent one queued it. false when the run keeps waiting for another step or has already left Waiting.

The push-back is stored before the run is queued. Until the task opens again, an identical repeated request only retries the queue and returns alreadyPushedBack: true; different feedback returns 409. After the task reopens, a new request starts a new iteration, so send expectedIteration to make a stale request fail instead of pushing back a task that already reopened. Feedback is limited to 16 KiB as JSON.

Error Responses:

  • 400: The body is not a JSON object, does not satisfy the stored push-back form, exceeds 16 KiB as recorded feedback, or the task declares no with.push_back.
  • 404: The DAG run or human-task step does not exist or is not visible to the caller.
  • 409: The task is not open, is at a different push-back iteration, was already pushed back with different feedback, or the run changed concurrently.
  • 413: The request body exceeds 16 MiB.
  • 503: The push-back was stored, but the DAG-run retry could not be queued.

A 503 with error code human_task_resume_failed includes pushBackStored: true and resumePending: true in details. Retry the same request or the resume endpoint; neither pushes the task back again.

Approval Endpoints ​

Separate manual-step APIs

The approve, reject, and push-back endpoints below resolve executable steps configured with approval. They do not operate on action: human.task; use Push Back Human Task instead.

DAG-run node responses expose actor names and IDs in approvedBy/approvedById, rejectedBy/rejectedById, and each pushBackHistory entry's by/byId. These fields may be absent on older runs or when authentication is disabled.

Approve Step ​

Endpoint: POST /api/v1/dag-runs/{name}/{dagRunId}/steps/{stepName}/approve

Approves a step that is in Waiting status and resumes the run when a step can run. In a root run, the resume happens even while other manual steps keep waiting; see Parallel Approvals. If the step's approval.input defines input fields, the provided inputs are available as environment variables in subsequent steps.

Request Body (optional):

json
{
  "inputs": {
    "FEEDBACK": "Looks good, proceed.",
    "PRIORITY": "high"
  }
}
FieldTypeDescription
inputsobjectKey-value pairs injected as environment variables in subsequent steps. Keys must match approval.input if defined.

Response (200):

json
{
  "dagRunId": "20240211_140000_abc123",
  "stepName": "deploy-staging",
  "resumed": true
}
FieldTypeDescription
dagRunIdstringThe DAG run ID
stepNamestringThe approved step name
resumedbooleanWhether resume execution was accepted, directly or through a queue. true does not mean execution has started.

Error Responses:

  • 400: Step is not in Waiting status, or required inputs missing
  • 404: DAG-run or step not found
  • 409: The approval was saved, but the run changed before its resume could be accepted
  • 503: The approval was saved, but the resume could not be accepted

A 503 with error code internal_error includes approvalStored: true and resumePending: true in details. Do not approve again. Retry only the resume with Resume After Approval.

Sub DAG variant: POST /api/v1/dag-runs/{name}/{dagRunId}/sub-dag-runs/{subDAGRunId}/steps/{stepName}/approve

The sub-DAG variant resumes the child only after every waiting step inside it is resolved. It returns resumed: false instead of 503 when that resume fails.

Resume After Approval ​

Endpoint: POST /api/v1/dag-runs/{name}/{dagRunId}/resume

Resumes a root run whose approval is saved but whose resume was not accepted. This endpoint has no request body; it never changes approvals or their inputs. It requires permission to run DAGs.

Response (200):

json
{
  "dagRunId": "20240211_140000_abc123",
  "resumed": true
}

The operation is safe to retry. A run that is already queued or running returns resumed: true without starting another execution.

Error Responses:

  • 404: The DAG run does not exist.
  • 409: The run has no saved root approval, no approved work is ready to resume, or the run changed before the resume could be accepted.
  • 503: The resume could not be accepted again and remains retryable.

Reject Step ​

Endpoint: POST /api/v1/dag-runs/{name}/{dagRunId}/steps/{stepName}/reject

Rejects a step that is in Waiting status. The step transitions to Rejected, the DAG status becomes rejected, dependent steps are aborted, and the failure handler runs.

Request Body (optional):

json
{
  "reason": "Output quality insufficient"
}
FieldTypeDescription
reasonstringOptional reason for rejection

Response (200):

json
{
  "dagRunId": "20240211_140000_abc123",
  "stepName": "deploy-staging"
}

Error Responses:

  • 400: Step is not in Waiting status
  • 404: DAG-run or step not found

Sub DAG variant: POST /api/v1/dag-runs/{name}/{dagRunId}/sub-dag-runs/{subDAGRunId}/steps/{stepName}/reject

Push Back Step ​

Endpoint: POST /api/v1/dag-runs/{name}/{dagRunId}/steps/{stepName}/push-back

Pushes back a Waiting step for re-execution with feedback. By default, the waiting approval step itself is the restart point. If approval.rewind_to is configured, Dagu resets that earlier step and all of its transitive dependents to NotStarted. The approvalIteration counter increments, and every step that later re-executes within the rewound scope receives the provided inputs as environment variables plus the DAG_PUSHBACK JSON payload.

Request Body (optional):

json
{
  "inputs": {
    "FEEDBACK": "Include error counts in the summary",
    "FORMAT": "markdown"
  }
}
FieldTypeDescription
inputsobjectKey-value pairs injected as environment variables when rewound steps re-execute. If a step declares approval.input, only those declared keys are exposed on that step.

Response (200):

json
{
  "dagRunId": "20240211_140000_abc123",
  "stepName": "generate-report",
  "approvalIteration": 2,
  "resumed": true
}
FieldTypeDescription
dagRunIdstringThe DAG run ID
stepNamestringThe pushed-back step name
approvalIterationintegerThe approval iteration count after push-back
resumedbooleanWhether the DAG run was re-enqueued for execution

Error Responses:

  • 400: Step is not in Waiting status, step has no approval config, or required inputs missing
  • 404: DAG-run or step not found

Sub DAG variant: POST /api/v1/dag-runs/{name}/{dagRunId}/sub-dag-runs/{subDAGRunId}/steps/{stepName}/push-back

Search Endpoints ​

Search DAGs ​

Endpoint: GET /api/v1/dags/search

Performs full-text search across DAG definitions. This endpoint remains available for direct DAG search requests.

Query Parameters:

ParameterTypeDescriptionRequired
qstringSearch queryYes
remoteNodestringRemote node nameNo

Response (200):

json
{
  "results": [
    {
      "name": "database_backup",
      "dag": {
        "name": "database_backup",
        "group": "Operations",
        "schedule": [{"expression": "0 0 * * 0"}],
        "description": "Weekly database backup job",
        "params": ["target_db", "retention_days"],
        "defaultParams": "{\"retention_days\": 30}",
        "labels": ["backup", "weekly", "critical"]
      },
      "matches": [
        {
          "line": "    command: pg_dump ${params.target_db} | gzip > backup_$(date +%Y%m%d).sql.gz",
          "lineNumber": 25,
          "startLine": 20
        },
        {
          "line": "description: Weekly database backup job",
          "lineNumber": 3,
          "startLine": 1
        }
      ]
    },
    {
      "name": "data_processing_pipeline",
      "dag": {
        "name": "data_processing_pipeline",
        "group": "ETL",
        "schedule": [
          {"expression": "0 2 * * *"},
          {"expression": "0 14 * * *"}
        ],
        "description": "Daily data processing pipeline for warehouse ETL",
        "params": ["date", "env", "batch_size"],
        "defaultParams": "{\"batch_size\": 1000, \"env\": \"dev\"}",
        "labels": ["production", "etl", "daily"]
      },
      "matches": [
        {
          "line": "      command: psql -h ${env.DB_HOST} -d analytics -c \"COPY data TO STDOUT\"",
          "lineNumber": 45,
          "startLine": 42
        }
      ]
    }
  ],
  "errors": []
}

Search DAG Feed ​

Endpoint: GET /api/v1/search/dags

Returns lightweight cursor-based DAG search results for the global search page.

Query Parameters:

ParameterTypeDescriptionRequired
qstringSearch queryYes
limitintegerNumber of results to return (max 50)No
cursorstringOpaque cursor returned by a previous responseNo
remoteNodestringRemote node nameNo
workspaceall, default, or workspace nameWorkspace to search. Omitted defaults to all.No

Each result includes preview snippets plus hasMoreMatches and nextMatchesCursor for loading more snippets from that result.

Response (200):

json
{
  "results": [
    {
      "fileName": "database_backup",
      "name": "database_backup",
      "hasMoreMatches": true,
      "nextMatchesCursor": "eyJmaWxlTmFtZSI6ImRhdGFiYXNlX2JhY2t1cCJ9",
      "matches": [
        {
          "line": "    command: pg_dump ${params.target_db} | gzip > backup_$(date +%Y%m%d).sql.gz",
          "lineNumber": 25,
          "startLine": 20
        }
      ]
    }
  ],
  "hasMore": true,
  "nextCursor": "eyJmaWxlTmFtZSI6ImRhdGFiYXNlX2JhY2t1cCJ9"
}

Search DAG Match Snippets ​

Endpoint: GET /api/v1/search/dags/{fileName}/matches

Loads additional cursor-based snippets for one DAG search result.

Query Parameters:

ParameterTypeDescriptionRequired
qstringSearch queryYes
limitintegerNumber of snippets to return (max 50)No
cursorstringOpaque cursor returned by a previous snippet responseNo
remoteNodestringRemote node nameNo
workspacedefault or workspace nameWorkspace for the selected DAG result. Omitted defaults to default.No

Response (200):

json
{
  "matches": [
    {
      "line": "description: Weekly database backup job",
      "lineNumber": 3,
      "startLine": 1
    }
  ],
  "hasMore": false
}

Event Logs ​

Endpoint: GET /api/v1/event-logs

Returns centralized operational event log entries. When builtin authentication is enabled, this endpoint requires a manager or admin role.

Query Parameters:

ParameterTypeDescriptionRequired
kindstringEvent kind such as dag_run or llm_usageNo
typestringEvent type such as dag.run.failed or llm.usage.recordedNo
dagNamestringFilter by DAG nameNo
dagRunIdstringFilter by DAG run IDNo
attemptIdstringFilter by attempt IDNo
sessionIdstringFilter by session IDNo
userIdstringFilter by user IDNo
modelstringFilter by model nameNo
startTimestringISO 8601 start time filterNo
endTimestringISO 8601 end time filterNo
limitintegerNumber of entries to return (max 500)No
offsetintegerOffset for compatibility paginationNo
paginationModestringoffset or cursorNo
cursorstringOpaque cursor for older entriesNo
remoteNodestringRemote node nameNo

If cursor is provided, cursor pagination is used automatically and offset must be omitted.

Response (200):

json
{
  "entries": [
    {
      "id": "dag_4ac3b3b9b16bdb10",
      "schemaVersion": 1,
      "occurredAt": "2026-04-05T08:05:12Z",
      "recordedAt": "2026-04-05T08:05:12Z",
      "kind": "dag_run",
      "type": "dag.run.failed",
      "sourceService": "scheduler",
      "sourceInstance": "worker-1:12345",
      "dagName": "nightly-report",
      "dagRunId": "20260405_080000_abc123",
      "attemptId": "attempt-1",
      "status": "failed",
      "data": {
        "trigger": "scheduler"
      }
    }
  ],
  "nextCursor": "eyJvY2N1cnJlZEF0IjoiMjAyNi0wNC0wNVQwODowNToxMloiLCJpZCI6ImRhZ180YWMzYjNiOWIxNmJkYjEwIn0"
}

Get All Labels ​

Endpoint: GET /api/v1/dags/labels

Retrieves all unique labels used across DAGs.

Query Parameters:

ParameterTypeDescriptionDefault
remoteNodestringRemote node name"local"

Response (200):

json
{
  "labels": [
    "backup",
    "critical",
    "daily",
    "data-quality",
    "etl",
    "experimental",
    "hourly",
    "maintenance",
    "ml",
    "monitoring",
    "production",
    "reporting",
    "staging",
    "testing",
    "weekly"
  ],
  "errors": []
}

Response with Errors (200):

json
{
  "labels": [
    "backup",
    "critical",
    "daily",
    "etl",
    "production"
  ],
  "errors": [
    "Error reading DAG file: malformed-etl.yaml - yaml: line 15: found unexpected end of stream",
    "Error reading DAG file: invalid-syntax.yaml - yaml: unmarshal errors:\n  line 8: field invalidField not found in type digraph.DAG"
  ]
}

Monitoring Endpoints ​

Prometheus Metrics ​

Endpoint: GET /api/v1/metrics

Returns Prometheus-compatible metrics.

Response (200) (text/plain):

text
# HELP dagu_info Dagu build information
# TYPE dagu_info gauge
dagu_info{version="1.14.0",build_date="2024-01-01T12:00:00Z",go_version="1.21"} 1

# HELP dagu_uptime_seconds Time since server start
# TYPE dagu_uptime_seconds gauge
dagu_uptime_seconds 3600

# HELP dagu_dag_runs_currently_running Number of currently running DAG runs
# TYPE dagu_dag_runs_currently_running gauge
dagu_dag_runs_currently_running 5

# HELP dagu_dag_runs_queued_total Total number of DAG runs in queue
# TYPE dagu_dag_runs_queued_total gauge
dagu_dag_runs_queued_total 8

# HELP dagu_dag_runs_total Total number of DAG runs by status
# TYPE dagu_dag_runs_total counter
dagu_dag_runs_total{status="succeeded"} 2493
dagu_dag_runs_total{status="failed"} 15
dagu_dag_runs_total{status="aborted"} 7

# HELP dagu_dags_total Total number of DAGs
# TYPE dagu_dags_total gauge
dagu_dags_total 45

# HELP dagu_scheduler_running Whether the scheduler is running
# TYPE dagu_scheduler_running gauge
dagu_scheduler_running 1

Error Handling ​

All endpoints return structured error responses:

json
{
  "code": "error_code",
  "message": "Human readable error message",
  "details": {
    "additional": "error details"
  }
}

Error Codes:

CodeDescription
forbiddenInsufficient permissions
bad_requestInvalid request parameters
not_foundResource doesn't exist
internal_errorServer-side error
unauthorizedAuthentication failed
bad_gatewayUpstream service error
remote_node_errorRemote node connection failed
already_runningDAG is already running
not_runningDAG is not running
already_existsResource already exists

Sub DAG Run Endpoints ​

Get Sub DAG Runs with Timing Information ​

Endpoint: GET /api/v1/dag-runs/{name}/{dagRunId}/sub-dag-runs

Retrieves timing and status information for all sub DAG runs within a specific DAG run. This is useful for tracking repeated executions of sub DAG steps and understanding the timeline of sub workflows.

Path Parameters:

ParameterTypeDescription
namestringDAG name
dagRunIdstringDAG run ID

Query Parameters:

ParameterTypeDescriptionDefault
remoteNodestringRemote node name"local"

Response (200):

json
{
  "subRuns": [
    {
      "dagRunId": "sub_20240211_143020_001",
      "params": "{\"file\": \"/tmp/data_batch_1.csv\", \"table\": \"fact_sales\"}",
      "status": 4,
      "statusLabel": "succeeded",
      "startedAt": "2024-02-11T14:30:20Z",
      "finishedAt": "2024-02-11T14:35:45Z"
    },
    {
      "dagRunId": "sub_20240211_143600_002",
      "params": "{\"file\": \"/tmp/data_batch_2.csv\", \"table\": \"fact_sales\"}",
      "status": 4,
      "statusLabel": "succeeded",
      "startedAt": "2024-02-11T14:36:00Z",
      "finishedAt": "2024-02-11T14:41:30Z"
    },
    {
      "dagRunId": "sub_20240211_144200_003",
      "params": "{\"file\": \"/tmp/data_batch_3.csv\", \"table\": \"fact_sales\"}",
      "status": 1,
      "statusLabel": "running",
      "startedAt": "2024-02-11T14:42:00Z",
      "finishedAt": null
    }
  ]
}

Response Fields:

  • subRuns: Array of sub DAG run details with timing information
  • dagRunId: Unique identifier for the sub DAG run
  • params: JSON string of parameters passed to the sub DAG
  • status: Execution status (0-8, see status values below)
  • statusLabel: Human-readable status label
  • startedAt: ISO 8601 timestamp when the sub DAG run started
  • finishedAt: ISO 8601 timestamp when the sub DAG run finished (null if still running)

Status Values:

  • 0: Not started
  • 1: Running
  • 2: Failed
  • 3: Aborted
  • 4: Success
  • 5: Queued
  • 6: Partial Success
  • 7: Waiting
  • 8: Rejected

Error Response (404):

json
{
  "code": "not_found",
  "message": "DAG run not found"
}

Use Case: This endpoint is particularly useful for nodes with repeat_policy that execute sub DAGs multiple times. It allows the UI to display a timeline of all executions with their respective start times, end times, and statuses, making it easier to track and debug repeated sub workflow executions.

Get Sub DAG Run Details ​

Endpoint: GET /api/v1/dag-runs/{name}/{dagRunId}/sub-dag-runs/{subDAGRunId}

Fetches detailed status of a sub DAG run.

Response (200):

json
{
  "dagRunDetails": {
    "rootDAGRunName": "data_processing_pipeline",
    "rootDAGRunId": "20240211_140000_abc123",
    "parentDAGRunName": "data_processing_pipeline",
    "parentDAGRunId": "20240211_140000_abc123",
    "dagRunId": "sub_20240211_143020_xyz456",
    "name": "warehouse_loader_subdag",
    "status": 4,
    "statusLabel": "succeeded",
    "queuedAt": "",
    "startedAt": "2024-02-11T14:30:20Z",
    "finishedAt": "2024-02-11T14:45:30Z",
    "log": "/logs/warehouse_loader_subdag/sub_20240211_143020_xyz456.log",
    "params": "{\"file\": \"/tmp/transformed_20240211.csv\", \"table\": \"fact_sales\"}",
    "nodes": [
      {
        "step": {
          "name": "validate_schema",
          "command": "validate_schema.py",
          "args": [],
          "depends": []
        },
        "stdout": "/logs/warehouse_loader_subdag/sub_20240211_143020_xyz456/validate_schema.stdout",
        "stderr": "/logs/warehouse_loader_subdag/sub_20240211_143020_xyz456/validate_schema.stderr",
        "startedAt": "2024-02-11T14:30:20Z",
        "finishedAt": "2024-02-11T14:30:35Z",
        "status": 4,
        "statusLabel": "succeeded",
        "retryCount": 0,
        "doneCount": 1,
        "subRuns": [],
        "subRunsRepeated": [],
        "error": ""
      },
      {
        "step": {
          "name": "load_data",
          "command": "load_to_warehouse.py",
          "depends": ["validate_schema"]
        },
        "stdout": "/logs/warehouse_loader_subdag/sub_20240211_143020_xyz456/load_data.stdout",
        "stderr": "/logs/warehouse_loader_subdag/sub_20240211_143020_xyz456/load_data.stderr",
        "startedAt": "2024-02-11T14:30:35Z",
        "finishedAt": "2024-02-11T14:45:30Z",
        "status": 4,
        "statusLabel": "succeeded",
        "retryCount": 0,
        "doneCount": 1,
        "subRuns": [],
        "subRunsRepeated": [],
        "error": ""
      }
    ],
    "onExit": null,
    "onSuccess": null,
    "onFailure": null,
    "onAbort": null,
    "preconditions": []
  }
}

Get Sub DAG Run Log ​

Endpoint: GET /api/v1/dag-runs/{name}/{dagRunId}/sub-dag-runs/{subDAGRunId}/log

Fetches the log for a sub DAG run.

Response (200):

json
{
  "content": "2024-02-11 14:30:20 INFO Starting sub DAG: warehouse_loader_subdag\n2024-02-11 14:30:20 INFO Parameters: {\"file\": \"/tmp/transformed_20240211.csv\", \"table\": \"fact_sales\"}\n2024-02-11 14:30:20 INFO Parent DAG: data_processing_pipeline (20240211_140000_abc123)\n2024-02-11 14:30:20 INFO Step 'validate_schema' started\n2024-02-11 14:30:22 INFO Schema validation: Checking table structure for 'fact_sales'\n2024-02-11 14:30:35 INFO Step 'validate_schema' completed successfully\n2024-02-11 14:30:35 INFO Step 'load_data' started\n2024-02-11 14:30:36 INFO Opening file: /tmp/transformed_20240211.csv\n2024-02-11 14:30:37 INFO File contains 50000 records\n2024-02-11 14:30:38 INFO Beginning bulk insert to warehouse.fact_sales\n2024-02-11 14:35:00 INFO Progress: 25000/50000 records loaded (50%)\n2024-02-11 14:40:00 INFO Progress: 45000/50000 records loaded (90%)\n2024-02-11 14:45:28 INFO All 50000 records loaded successfully\n2024-02-11 14:45:29 INFO Committing transaction\n2024-02-11 14:45:30 INFO Step 'load_data' completed successfully\n2024-02-11 14:45:30 INFO Sub DAG completed successfully\n",
  "lineCount": 7,
  "totalLines": 7,
  "hasMore": false,
  "isEstimate": false
}

Get Child Step Log ​

Endpoint: GET /api/v1/dag-runs/{name}/{dagRunId}/sub-dag-runs/{subDAGRunId}/steps/{stepName}/log

Fetches the log for a step in a sub DAG run.

Response (200):

json
{
  "content": "2024-02-11 14:30:35 INFO Step 'load_data' started\n2024-02-11 14:30:36 INFO Opening file: /tmp/transformed_20240211.csv\n2024-02-11 14:30:37 INFO File contains 50000 records\n2024-02-11 14:30:38 INFO Beginning bulk insert to warehouse.fact_sales\n2024-02-11 14:30:39 INFO Using batch size: 5000\n2024-02-11 14:30:40 INFO Processing batch 1 of 10\n2024-02-11 14:32:00 INFO Batch 1 complete (5000 records)\n2024-02-11 14:33:20 INFO Processing batch 2 of 10\n2024-02-11 14:34:40 INFO Batch 2 complete (10000 records)\n2024-02-11 14:36:00 INFO Processing batch 3 of 10\n2024-02-11 14:37:20 INFO Batch 3 complete (15000 records)\n2024-02-11 14:38:40 INFO Processing batch 4 of 10\n2024-02-11 14:40:00 INFO Batch 4 complete (20000 records)\n2024-02-11 14:41:20 INFO Processing batch 5 of 10\n2024-02-11 14:42:40 INFO Batch 5 complete (25000 records)\n[... truncated for brevity ...]\n2024-02-11 14:45:28 INFO All 50000 records loaded successfully\n2024-02-11 14:45:29 INFO Committing transaction\n2024-02-11 14:45:30 INFO Step 'load_data' completed successfully\n",
  "lineCount": 50,
  "totalLines": 156,
  "hasMore": true,
  "isEstimate": false
}

Update Child Step Status ​

Endpoint: PATCH /api/v1/dag-runs/{name}/{dagRunId}/sub-dag-runs/{subDAGRunId}/steps/{stepName}/status

Updates the status of a step in a sub DAG run.

Request Body:

json
{
  "status": 4
}

Response (200): Success

Error Response (404):

json
{
  "code": "not_found",
  "message": "Sub DAG run or step not found"
}

Queue Management Endpoints ​

List All Queues ​

Endpoint: GET /api/v1/queues

Retrieves queue summaries. Use GET /api/v1/queues/{name}/items to fetch paginated running or queued items for a specific queue.

Response (200):

json
{
  "queues": [
    {
      "name": "critical",
      "type": "global",
      "maxConcurrency": 2,
      "runningCount": 1,
      "queuedCount": 2,
      "running": [
        {
          "dagRunId": "20240211_140000_abc123",
          "name": "data_processing_pipeline",
          "status": 1,
          "statusLabel": "running",
          "startedAt": "2024-02-11T14:00:00Z",
          "finishedAt": "",
          "log": "/logs/data_processing_pipeline/20240211_140000_abc123.log"
        }
      ]
    },
    {
      "name": "backup_job",
      "type": "dag-based",
      "runningCount": 1,
      "queuedCount": 0,
      "running": [
        {
          "dagRunId": "20240211_150000_backup",
          "name": "backup_job",
          "status": 1,
          "statusLabel": "running",
          "startedAt": "2024-02-11T15:00:00Z",
          "finishedAt": "",
          "log": "/logs/backup_job/20240211_150000_backup.log"
        }
      ],
      "maxConcurrency": 1
    }
  ],
  "summary": {
    "totalQueues": 2,
    "totalRunning": 2,
    "totalQueued": 2,
    "totalCapacity": 3,
    "utilizationPercentage": 66.666664
  }
}

Response Fields:

  • queues: Queue summary objects
  • type: global for configured global queues, dag-based when the DAG name itself acts as the queue
  • running: Only currently running DAG-runs are embedded here
  • summary: Capacity and utilization totals across all queues

List Queue Items ​

Endpoint: GET /api/v1/queues/{name}/items

Returns paginated items for one queue.

Query Parameters:

ParameterTypeDescriptionDefault
typestringqueued or runningqueued
pageintegerPage number1
perPageintegerItems per page50
remoteNodestringRemote node name"local"

Response (200):

json
{
  "items": [
    {
      "dagRunId": "20240211_143000_def456",
      "name": "ml_training_pipeline",
      "status": 5,
      "statusLabel": "queued",
      "queuedAt": "2024-02-11T14:30:00Z",
      "startedAt": "",
      "finishedAt": "",
      "log": "/logs/ml_training_pipeline/20240211_143000_def456.log",
      "conditions": [
        {
          "type": "Queued",
          "status": "True",
          "reason": "QueueConcurrencyLimitReached",
          "message": "DAG-run is waiting because the queue's active-run concurrency limit has been reached.",
          "checkedAt": "2024-02-11T14:31:00Z"
        }
      ]
    }
  ],
  "pagination": {
    "totalRecords": 2,
    "currentPage": 1,
    "totalPages": 2,
    "nextPage": 2,
    "prevPage": null
  }
}

Error Response (500):

json
{
  "code": "internal_error",
  "message": "Failed to retrieve queue information"
}

Queued items use the same conditions field as other DAG-run summaries. Running queue items do not include queued runtime conditions.

Additional Endpoints ​

List DAG Runs by Name ​

Endpoint: GET /api/v1/dag-runs/{name}

Lists DAG runs for a specific DAG name using forward-only cursor pagination.

Path Parameters:

ParameterTypeDescription
namestringDAG name

Query Parameters:

ParameterTypeDescriptionDefault
statusintegerFilter by status (0-8)-
fromDateintegerUnix timestamp start-
toDateintegerUnix timestamp end-
dagRunIdstringFilter by run ID-
limitintegerPage size (max 500)100
cursorstringOpaque cursor returned by the previous page-
remoteNodestringRemote node name"local"

Use nextCursor from the previous response to continue loading older runs.

Response (200):

json
{
  "dagRuns": [
    {
      "rootDAGRunName": "data_processing_pipeline",
      "rootDAGRunId": "20240211_140000_abc123",
      "parentDAGRunName": "",
      "parentDAGRunId": "",
      "dagRunId": "20240211_140000_abc123",
      "name": "data_processing_pipeline",
      "status": 4,
      "statusLabel": "succeeded",
      "queuedAt": "",
      "startedAt": "2024-02-11T14:00:00Z",
      "finishedAt": "2024-02-11T14:45:30Z",
      "log": "/logs/data_processing_pipeline/20240211_140000_abc123.log",
      "params": "{\"date\": \"2024-02-11\", \"env\": \"production\", \"batch_size\": 5000}"
    },
    {
      "rootDAGRunName": "data_processing_pipeline",
      "rootDAGRunId": "20240211_020000_def456",
      "parentDAGRunName": "",
      "parentDAGRunId": "",
      "dagRunId": "20240211_020000_def456",
      "name": "data_processing_pipeline",
      "status": 2,
      "statusLabel": "failed",
      "queuedAt": "",
      "startedAt": "2024-02-11T02:00:00Z",
      "finishedAt": "2024-02-11T02:15:45Z",
      "log": "/logs/data_processing_pipeline/20240211_020000_def456.log",
      "params": "{\"date\": \"2024-02-11\", \"env\": \"production\", \"batch_size\": 5000}"
    }
  ],
  "nextCursor": "eyJvY2N1cnJlZEF0IjoiMjAyNC0wMi0xMVQxNDowMDowMFoiLCJkYWdSdW5JZCI6IjIwMjQwMjExXzE0MzAwMF9tbCJ9"
}

Example Usage ​

Start a DAG with Parameters ​

bash
curl -X POST "http://localhost:8080/api/v1/dags/data-processing-pipeline/start" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer your-token" \
     -d '{
       "params": "{\"date\": \"2024-02-11\", \"env\": \"production\", \"batch_size\": 5000}",
       "dagRunId": "manual_20240211_160000"
     }'

Response:

json
{
  "dagRunId": "manual_20240211_160000"
}

Start a DAG with Singleton Mode ​

bash
curl -X POST "http://localhost:8080/api/v1/dags/critical-job/start" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer your-token" \
     -d '{
       "singleton": true,
       "params": "{\"priority\": \"high\"}"
     }'

Response if DAG is not running (200):

json
{
  "dagRunId": "20240211_161500_xyz789"
}

Response if DAG is already running (409):

json
{
  "code": "already_running",
  "message": "DAG critical-job is already running, cannot start in singleton mode"
}

Check DAG Run Status ​

bash
curl "http://localhost:8080/api/v1/dag-runs/data-processing-pipeline/latest" \
     -H "Authorization: Bearer your-token"

Response:

json
{
  "dagRunDetails": {
    "rootDAGRunName": "data_processing_pipeline",
    "rootDAGRunId": "20240211_160000_current",
    "parentDAGRunName": "",
    "parentDAGRunId": "",
    "dagRunId": "20240211_160000_current",
    "name": "data_processing_pipeline",
    "status": 1,
    "statusLabel": "running",
    "queuedAt": "",
    "startedAt": "2024-02-11T16:00:00Z",
    "finishedAt": "",
    "log": "/logs/data_processing_pipeline/20240211_160000_current.log",
    "params": "{\"date\": \"2024-02-11\", \"env\": \"production\", \"batch_size\": 5000}",
    "nodes": [
      {
        "step": {
          "name": "extract_data",
          "id": "extract"
        },
        "status": 4,
        "statusLabel": "succeeded",
        "startedAt": "2024-02-11T16:00:30Z",
        "finishedAt": "2024-02-11T16:15:45Z",
        "retryCount": 0,
        "doneCount": 1
      },
      {
        "step": {
          "name": "transform_data",
          "id": "transform"
        },
        "status": 1,
        "statusLabel": "running",
        "startedAt": "2024-02-11T16:15:45Z",
        "finishedAt": "",
        "retryCount": 0,
        "doneCount": 0
      },
      {
        "step": {
          "name": "load_to_warehouse",
          "id": "load"
        },
        "status": 0,
        "statusLabel": "not_started",
        "startedAt": "",
        "finishedAt": "",
        "retryCount": 0,
        "doneCount": 0
      }
    ]
  }
}

Search for DAGs ​

bash
curl "http://localhost:8080/api/v1/dags/search?q=database" \
     -H "Authorization: Bearer your-token"

Response:

json
{
  "results": [
    {
      "name": "database_backup",
      "dag": {
        "name": "database_backup",
        "group": "Operations",
        "schedule": [{"expression": "0 0 * * 0"}],
        "description": "Weekly database backup job",
        "params": ["target_db", "retention_days"],
        "defaultParams": "{\"retention_days\": 30}",
        "labels": ["backup", "weekly", "critical"]
      },
      "matches": [
        {
          "line": "    command: pg_dump ${params.target_db} | gzip > backup_$(date +%Y%m%d).sql.gz",
          "lineNumber": 25,
          "startLine": 20
        }
      ]
    }
  ],
  "errors": []
}

Get Metrics for Monitoring ​

bash
curl "http://localhost:8080/api/v1/metrics" | grep dagu_dag_runs_currently_running

Stop a Running DAG ​

bash
curl -X POST "http://localhost:8080/api/v1/dag-runs/data-pipeline/20240211_120000/stop" \
     -H "Authorization: Bearer your-token"

Update Step Status Manually ​

bash
# Mark a failed step as successful
curl -X PATCH "http://localhost:8080/api/v1/dag-runs/data-processing-pipeline/20240211_020000_def456/steps/transform_data/status" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer your-token" \
     -d '{"status": 4}'

Response (200): Success (empty response body)

Approve a Waiting Step ​

bash
curl -X POST "http://localhost:8080/api/v1/dag-runs/deploy-pipeline/20240211_120000/steps/deploy-staging/approve" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer your-token" \
     -d '{"inputs": {"APPROVED_BY": "ops-team"}}'

Reject a Waiting Step ​

bash
curl -X POST "http://localhost:8080/api/v1/dag-runs/deploy-pipeline/20240211_120000/steps/deploy-staging/reject" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer your-token" \
     -d '{"reason": "Not ready for production"}'

Push Back a Waiting Step ​

bash
curl -X POST "http://localhost:8080/api/v1/dag-runs/report-pipeline/20240211_120000/steps/generate-report/push-back" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer your-token" \
     -d '{"inputs": {"FEEDBACK": "Include error counts"}}'

Enqueue a DAG Run ​

bash
curl -X POST "http://localhost:8080/api/v1/dags/ml-training-pipeline/enqueue" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer your-token" \
     -d '{
       "params": "{\"model\": \"recommendation_v3\", \"dataset\": \"user_interactions_2024\"}",
       "dagRunId": "ml_train_20240211_170000",
       "dagName": "ml-training-on-demand",
       "queue": "gpu-jobs"
     }'

Response:

json
{
  "dagRunId": "ml_train_20240211_170000"
}

Get Logs with Pagination ​

bash
# Get last 100 lines of a DAG run log
curl "http://localhost:8080/api/v1/dag-runs/etl-pipeline/20240211_120000/log?tail=100"

# Get specific step's stderr output
curl "http://localhost:8080/api/v1/dag-runs/etl-pipeline/20240211_120000/steps/transform/log?stream=stderr"

# Get logs with offset and limit
curl "http://localhost:8080/api/v1/dag-runs/etl-pipeline/20240211_120000/log?offset=1000&limit=500"

Working with Sub DAGs ​

bash
# Get all sub DAG runs with timing information
curl "http://localhost:8080/api/v1/dag-runs/data-processing-pipeline/20240211_140000_abc123/sub-dag-runs" \
     -H "Authorization: Bearer your-token"

# Get sub DAG run details
curl "http://localhost:8080/api/v1/dag-runs/data-processing-pipeline/20240211_140000_abc123/sub-dag-runs/sub_20240211_143020_xyz456" \
     -H "Authorization: Bearer your-token"

# Get sub DAG step log
curl "http://localhost:8080/api/v1/dag-runs/data-processing-pipeline/20240211_140000_abc123/sub-dag-runs/sub_20240211_143020_xyz456/steps/load_data/log" \
     -H "Authorization: Bearer your-token"

# Update sub DAG step status
curl -X PATCH "http://localhost:8080/api/v1/dag-runs/data-processing-pipeline/20240211_140000_abc123/sub-dag-runs/sub_20240211_143020_xyz456/steps/load_data/status" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer your-token" \
     -d '{"status": 4}'

Rename a DAG ​

bash
curl -X POST "http://localhost:8080/api/v1/dags/old-pipeline-name/rename" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer your-token" \
     -d '{"newFileName": "new-pipeline-name"}'

Response (200): Success (empty response body)

Delete a DAG ​

bash
curl -X DELETE "http://localhost:8080/api/v1/dags/deprecated-pipeline" \
     -H "Authorization: Bearer your-token"

Response (204): No content (successful deletion)

Get DAG Specification YAML ​

bash
curl "http://localhost:8080/api/v1/dags/data-processing-pipeline/spec" \
     -H "Authorization: Bearer your-token"

Response:

json
{
  "dag": {
    "name": "data_processing_pipeline",
    "group": "ETL"
  },
  "spec": "group: ETL\nschedule:\n  - \"0 2 * * *\"\n  - \"0 14 * * *\"\ndescription: Daily data processing pipeline for warehouse ETL\nenv:\n  DATA_SOURCE: postgres://prod-db:5432/analytics\nlog_dir: /var/log/dagu/pipelines\nhist_retention_days: 30\nmax_active_steps: 5\nparams:\n  - name: date\n    type: string\n    required: true\n  - name: environment\n    type: string\n    default: dev\n    enum: [dev, staging, prod]\n  - name: batch_size\n    type: integer\n    default: 1000\n    minimum: 1\n    maximum: 10000\nlabels: [production, etl, daily]\npreconditions:\n  - eval: \"$(date +%u)\"\n    expected: \"re:[1-5]\"\nsteps:\n  - id: extract\n    description: Extract data from source database\n    working_dir: /app/etl\n    run: python extract.py --date \"${params.date}\"\n    stdout: /logs/extract.out\n    stderr: /logs/extract.err\n\n  - id: transform\n    description: Apply transformations to extracted data\n    run: python transform.py\n    depends: [extract]\n\n  - id: load\n    run: warehouse-loader --batch-size \"${params.batch_size}\"\n    depends: [transform]\nhandler_on:\n  success:\n    run: notify.sh \"Pipeline completed successfully\"\n  failure:\n    run: alert.sh \"Pipeline failed\" high\n  exit:\n    run: cleanup_temp_files.sh",
  "errors": []
}

Complex Filtering Examples ​

bash
# Get all failed DAG runs in the last 24 hours
curl "http://localhost:8080/api/v1/dag-runs?status=2&fromDate=$(date -d '24 hours ago' +%s)" \
     -H "Authorization: Bearer your-token"

# Get DAG runs for a specific DAG with cursor pagination
curl "http://localhost:8080/api/v1/dag-runs?name=data-processing-pipeline&limit=20" \
     -H "Authorization: Bearer your-token"

# Search for DAGs with specific labels
curl "http://localhost:8080/api/v1/dags?labels=production&page=1&perPage=50" \
     -H "Authorization: Bearer your-token"

# Get running DAG runs
curl "http://localhost:8080/api/v1/dag-runs?status=1" \
     -H "Authorization: Bearer your-token"

# Get queued DAG runs
curl "http://localhost:8080/api/v1/dag-runs?status=5" \
     -H "Authorization: Bearer your-token"

# View all execution queues with running and queued DAG runs
curl "http://localhost:8080/api/v1/queues" \
     -H "Authorization: Bearer your-token"

# Load queued items from one queue
curl "http://localhost:8080/api/v1/queues/default/items?type=queued&page=1&perPage=20" \
     -H "Authorization: Bearer your-token"

Pause/Resume the Scheduler ​

bash
# Pause every schedule
curl -X POST "http://localhost:8080/api/v1/services/scheduler/pause" \
     -H "Authorization: Bearer $DAGU_API_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"paused": true, "reason": "database migration"}'

# Check the current state
curl "http://localhost:8080/api/v1/services/scheduler/pause" \
     -H "Authorization: Bearer $DAGU_API_TOKEN"

# Resume
curl -X POST "http://localhost:8080/api/v1/services/scheduler/pause" \
     -H "Authorization: Bearer $DAGU_API_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"paused": false}'

Suspend/Resume DAG Scheduling ​

bash
# Suspend a DAG
curl -X POST "http://localhost:8080/api/v1/dags/data-processing-pipeline/suspend" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer your-token" \
     -d '{"suspend": true}'

# Resume a DAG
curl -X POST "http://localhost:8080/api/v1/dags/data-processing-pipeline/suspend" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer your-token" \
     -d '{"suspend": false}'

Response (200): Success (empty response body)

API Response Status Codes Summary ​

Status CodeDescriptionCommon Scenarios
200SuccessSuccessful GET, POST, PUT, PATCH requests
201CreatedNew DAG created successfully
204No ContentSuccessful DELETE operation
400Bad RequestInvalid parameters, malformed JSON, invalid DAG name format
401UnauthorizedMissing or invalid authentication token
403ForbiddenInsufficient permissions (e.g., no write access)
404Not FoundDAG, DAG run, or resource doesn't exist
409ConflictResource already exists (e.g., DAG name conflict)
500Internal ErrorServer-side processing error
503Service UnavailableServer unhealthy or scheduler not responding

API Key Endpoints ​

API key management endpoints require Builtin Authentication mode and admin role.

List API Keys ​

Endpoint: GET /api/v1/api-keys

Retrieves all API keys. Requires admin role.

Response (200):

json
{
  "apiKeys": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "ci-pipeline",
      "description": "API key for CI/CD pipeline",
      "role": "operator",
      "keyPrefix": "dagu_7Kq",
      "createdAt": "2024-02-11T12:00:00Z",
      "updatedAt": "2024-02-11T12:00:00Z",
      "createdBy": "admin-user-id",
      "lastUsedAt": "2024-02-11T15:30:00Z"
    }
  ]
}

Create API Key ​

Endpoint: POST /api/v1/api-keys

Creates a new API key. Requires admin role.

Request Body:

json
{
  "name": "ci-pipeline",
  "description": "API key for CI/CD pipeline",
  "role": "operator"
}

Request Fields:

FieldTypeDescriptionRequired
namestringHuman-readable name (1-100 chars)Yes
descriptionstringOptional description (max 500 chars)No
rolestringRole: "admin", "manager", "operator", "viewer"Yes

Response (201):

json
{
  "apiKey": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "ci-pipeline",
    "description": "API key for CI/CD pipeline",
    "role": "operator",
    "keyPrefix": "dagu_7Kq",
    "createdAt": "2024-02-11T12:00:00Z",
    "updatedAt": "2024-02-11T12:00:00Z",
    "createdBy": "admin-user-id"
  },
  "key": "dagu_7Kq9mXxN3pLwR5tY2vZa8bCdEfGhJk4n6sUwXy0zA1B"
}

WARNING

The key field contains the full API key secret and is only returned once at creation time. Store it securely.

Error Response (409):

json
{
  "code": "already_exists",
  "message": "API key with this name already exists"
}

Get API Key ​

Endpoint: GET /api/v1/api-keys/{keyId}

Retrieves a specific API key by ID. Requires admin role.

Response (200):

json
{
  "apiKey": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "ci-pipeline",
    "description": "API key for CI/CD pipeline",
    "role": "operator",
    "keyPrefix": "dagu_7Kq",
    "createdAt": "2024-02-11T12:00:00Z",
    "updatedAt": "2024-02-11T12:00:00Z",
    "createdBy": "admin-user-id",
    "lastUsedAt": "2024-02-11T15:30:00Z"
  }
}

Error Response (404):

json
{
  "code": "not_found",
  "message": "API key not found"
}

Update API Key ​

Endpoint: PATCH /api/v1/api-keys/{keyId}

Updates an API key's metadata. Requires admin role.

Request Body:

json
{
  "name": "production-ci",
  "description": "Updated description",
  "role": "manager"
}

All fields are optional. Only provided fields are updated.

Response (200):

json
{
  "apiKey": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "production-ci",
    "description": "Updated description",
    "role": "manager",
    "keyPrefix": "dagu_7Kq",
    "createdAt": "2024-02-11T12:00:00Z",
    "updatedAt": "2024-02-11T16:00:00Z",
    "createdBy": "admin-user-id",
    "lastUsedAt": "2024-02-11T15:30:00Z"
  }
}

Error Response (409):

json
{
  "code": "already_exists",
  "message": "API key with this name already exists"
}

Delete API Key ​

Endpoint: DELETE /api/v1/api-keys/{keyId}

Deletes (revokes) an API key. Requires admin role.

Response (204): No content

Error Response (404):

json
{
  "code": "not_found",
  "message": "API key not found"
}

Example: Full API Key Workflow ​

bash
# 1. Login as admin to get JWT token
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "admin-password"}' | jq -r '.token')

# 2. Create an API key
API_KEY=$(curl -s -X POST http://localhost:8080/api/v1/api-keys \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "automation", "role": "operator"}' | jq -r '.key')

echo "Created API key: $API_KEY"

# 3. Use the API key for subsequent requests
curl -H "Authorization: Bearer $API_KEY" \
  http://localhost:8080/api/v1/dags

# 4. Start a DAG with the API key
curl -X POST http://localhost:8080/api/v1/dags/my-dag/start \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"params": "{\"env\": \"production\"}"}'

Webhook Endpoints ​

Webhooks expose a DAG-specific trigger endpoint and a separate set of management endpoints.

  • Trigger endpoint: POST /api/v1/webhooks/{fileName}
  • Management endpoints: developer, manager, or admin only when builtin auth is enabled
  • Per-DAG limit: one webhook configuration per DAG

Trigger DAG via Webhook ​

Endpoint: POST /api/v1/webhooks/{fileName}

The trigger endpoint uses the webhook's configured authentication mode instead of regular API authentication. New webhooks default to token-only authentication; other modes can require HMAC or both token and HMAC.

Path Parameters:

ParameterTypeDescription
fileNamestringDAG file name

Request Headers:

HeaderDescription
AuthorizationBearer dagu_wh_<token>. Accepts the webhook's default token or one of its profile tokens. Required when the webhook auth mode includes token authentication.
X-Dagu-SignatureHMAC-SHA256 signature as sha256=<hex>. Required when strict HMAC enforcement is active.
X-Dagu-ProfileOptional runtime profile for the run. With the default token, the profile must be in the webhook's profile-selection allowlist. With a profile token, the run always uses the token's profile, and the header may only name that profile.

Request Body (optional):

json
{
  "dagRunId": "deploy-abc123",
  "payload": {
    "branch": "main",
    "commit": "abc123"
  }
}

Request Fields:

FieldTypeDescriptionRequired
dagRunIdstringOptional idempotency key. If a run with this ID already exists, the endpoint returns 409 Conflict.No
payloadobjectArbitrary JSON exposed to the DAG as WEBHOOK_PAYLOAD.No

Payload behavior is slightly broader than the schema shape:

  • If the request body includes a top-level payload field, Dagu serializes that field into WEBHOOK_PAYLOAD.
  • Otherwise, if the raw request body is valid JSON, Dagu serializes the entire raw body into WEBHOOK_PAYLOAD.
  • Otherwise WEBHOOK_PAYLOAD is {}.
  • If you want a custom dagRunId without mixing it into WEBHOOK_PAYLOAD, use the wrapper form shown above.
  • The maximum accepted request body is controlled by webhooks.max_payload_size in config.yaml and defaults to 1048576 bytes.

If X-Dagu-Profile is omitted, Dagu uses the DAG's normal default-profile resolution. When HMAC is enabled, sign the exact raw request body if the profile header is absent. If the header is present, sign:

text
x-dagu-profile:<profile>\n<raw-request-body>

The prefix contains the lowercase literal x-dagu-profile, a colon, the selected profile, and one newline byte. The header and body are therefore both covered by the signature. See Select a Runtime Profile for a complete example.

Response (200):

json
{
  "dagRunId": "0196808c-04ff-73bb-a63e-83791b321ac0",
  "dagName": "my-dag"
}

Error Response (401):

json
{
  "code": "unauthorized",
  "message": "missing or invalid authorization header"
}

Another 401 response is invalid webhook token.

Missing or invalid HMAC signatures also return 401 when strict HMAC enforcement is active.

Error Response (403):

json
{
  "code": "forbidden",
  "message": "webhook is disabled"
}

Error Response (404):

json
{
  "code": "not_found",
  "message": "DAG my-dag not found"
}

Other 404 responses include no webhook configured for this DAG and webhook triggering is not configured on this server.

Profile-selection errors include:

  • 400 Bad Request for an invalid or repeated X-Dagu-Profile header, or a disabled selected profile
  • 403 Forbidden when the selected profile is not in the webhook allowlist, or a profile token was sent with a header naming a different profile
  • 404 Not Found when an allowlisted profile no longer exists

Error Response (409):

json
{
  "code": "already_exists",
  "message": "dag-run with ID deploy-abc123 already exists"
}

Error Response (413):

json
{
  "code": "bad_request",
  "message": "webhook payload too large (max 1048576 bytes)"
}

Webhook Details Shape ​

Webhook management endpoints return the same public webhook object:

json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "dagName": "my-dag",
  "tokenPrefix": "dagu_wh_7Kq9",
  "enabled": true,
  "authMode": "token_only",
  "hmac": {
    "enabled": false,
    "secretConfigured": false
  },
  "profileSelection": {
    "allowedProfiles": ["staging", "prod"]
  },
  "profileTokens": [
    {
      "id": "8f14e45f-ceea-467f-a8f4-1c2b3d4e5f60",
      "name": "customer-a",
      "tokenPrefix": "dagu_wh_Pq3R",
      "profile": "customer-a",
      "createdAt": "2026-04-29T10:30:00Z",
      "createdBy": "user-id",
      "lastUsedAt": "2026-04-29T10:45:00Z"
    }
  ],
  "createdAt": "2026-04-29T10:00:00Z",
  "updatedAt": "2026-04-29T10:15:00Z",
  "createdBy": "user-id",
  "lastUsedAt": "2026-04-29T10:20:00Z",
  "public": {
    "sender": "github",
    "secretConfigured": true,
    "events": ["push", "pull_request.opened"],
    "createdAt": "2026-04-29T11:00:00Z"
  }
}

Notes:

  • tokenPrefix is the stored identification prefix from the full token, currently the first 12 characters when available.
  • profileSelection.allowedProfiles lists the runtime profiles callers may select with X-Dagu-Profile. An empty list disables caller selection.
  • profileTokens lists the webhook's profile tokens without their secrets. lastUsedAt is set once a token has authorized a request. Remote nodes running versions without profile tokens omit the field.
  • lastUsedAt on the webhook and on each profile token is updated at most once per minute, so it can lag the latest request by up to a minute.
  • public is present while the webhook has a public URL. sender is absent when anyone with the URL may send. The URL itself comes from Get Public URL.

List All Webhooks ​

Endpoint: GET /api/v1/webhooks

Lists all configured webhooks across DAGs. Requires developer, manager, or admin role.

Response (200):

json
{
  "webhooks": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "dagName": "my-dag",
      "tokenPrefix": "dagu_wh_7Kq9",
      "enabled": true,
      "createdAt": "2026-04-29T10:00:00Z",
      "updatedAt": "2026-04-29T10:00:00Z",
      "createdBy": "user-id",
      "lastUsedAt": "2026-04-29T10:20:00Z"
    }
  ]
}

Get DAG Webhook ​

Endpoint: GET /api/v1/dags/{fileName}/webhook

Returns the webhook configuration for one DAG. Requires developer, manager, or admin role.

Error Response (404):

json
{
  "code": "not_found",
  "message": "no webhook configured for DAG my-dag"
}

Create DAG Webhook ​

Endpoint: POST /api/v1/dags/{fileName}/webhook

Creates the webhook record and returns the full token once. Requires developer, manager, or admin role.

Response (201):

json
{
  "webhook": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "dagName": "my-dag",
    "tokenPrefix": "dagu_wh_7Kq9",
    "enabled": true,
    "createdAt": "2026-04-29T10:00:00Z",
    "updatedAt": "2026-04-29T10:00:00Z",
    "createdBy": "user-id"
  },
  "token": "dagu_wh_7Kq9mXxN3pLwR5tY2vZa8bCdEfGhJk4n6sUwXy0zA1B"
}

The token field is returned only at creation and token-regeneration time.

Configure Webhook Profile Selection ​

Endpoint: PUT /api/v1/dags/{fileName}/webhook/profile-selection

Replaces the runtime profiles that callers may select with X-Dagu-Profile. This endpoint is admin-only.

Request Body:

json
{
  "allowedProfiles": ["staging", "prod"]
}

allowedProfiles is required. Each profile must exist and be active. Send [] to explicitly clear the policy and disable caller selection. Missing or null allowedProfiles values return 400 Bad Request.

Response (200): The updated webhook details.

Policy changes apply to subsequent trigger requests immediately; the server does not need to be restarted.

Error Responses:

  • 400 Bad Request: invalid, disabled, or missing profile input
  • 404 Not Found: webhook or requested runtime profile not found

Create Webhook Profile Token ​

Endpoint: POST /api/v1/dags/{fileName}/webhook/profile-tokens

Creates an additional webhook token bound to one runtime profile and returns the full token once. Requests authenticated with it always run with that profile. This endpoint is admin-only.

Request Body:

json
{
  "name": "customer-a",
  "profile": "customer-a"
}
FieldTypeDescriptionRequired
namestringLabel for the caller that holds the token, 1 to 100 characters.Yes
profilestringRuntime profile the token runs with. It must exist and be active.Yes

Response (201): The updated webhook details as webhook and the full token as token, in the same shape as Create DAG Webhook.

Error Responses:

  • 400 Bad Request: invalid name, disabled profile, webhook auth mode is hmac_only, or the webhook already has 100 profile tokens
  • 404 Not Found: webhook or runtime profile not found

Revoke Webhook Profile Token ​

Endpoint: DELETE /api/v1/dags/{fileName}/webhook/profile-tokens/{tokenId}

Revokes a profile token. The token is rejected from the next request on. This endpoint is admin-only.

Response (200): The updated webhook details.

Error Responses:

  • 404 Not Found: webhook or profile token not found

Regenerate Webhook Token ​

Endpoint: POST /api/v1/dags/{fileName}/webhook/regenerate

Rotates the webhook token and invalidates the old token immediately. Requires developer, manager, or admin role.

Response (200):

json
{
  "webhook": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "dagName": "my-dag",
    "tokenPrefix": "dagu_wh_9XzN",
    "enabled": true,
    "createdAt": "2026-04-29T10:00:00Z",
    "updatedAt": "2026-04-29T10:30:00Z",
    "createdBy": "user-id"
  },
  "token": "dagu_wh_9XzNewTokenGeneratedHereAfterRegeneration"
}

Toggle Webhook ​

Endpoint: POST /api/v1/dags/{fileName}/webhook/toggle

Enables or disables the webhook without rotating credentials. Requires developer, manager, or admin role.

Request Body:

json
{
  "enabled": false
}

Response (200):

json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "dagName": "my-dag",
  "tokenPrefix": "dagu_wh_7Kq9",
  "enabled": false,
  "createdAt": "2026-04-29T10:00:00Z",
  "updatedAt": "2026-04-29T10:30:00Z",
  "createdBy": "user-id",
  "lastUsedAt": "2026-04-29T10:20:00Z"
}

Delete DAG Webhook ​

Endpoint: DELETE /api/v1/dags/{fileName}/webhook

Deletes the webhook configuration. Requires developer, manager, or admin role.

Response (204): No content

Example: Using Webhooks ​

bash
# 1. Login with a user that has developer, manager, or admin role
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "your-username", "password": "your-password"}' | jq -r '.token')

# 2. Create a webhook for a DAG
WEBHOOK=$(curl -s -X POST http://localhost:8080/api/v1/dags/my-dag/webhook \
  -H "Authorization: Bearer $TOKEN")
WEBHOOK_TOKEN=$(echo $WEBHOOK | jq -r '.token')

# 3. Trigger the DAG using the webhook
curl -X POST http://localhost:8080/api/v1/webhooks/my-dag \
  -H "Authorization: Bearer $WEBHOOK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"payload": {"branch": "main", "commit": "abc123"}}'

# 4. Regenerate token if compromised
curl -X POST http://localhost:8080/api/v1/dags/my-dag/webhook/regenerate \
  -H "Authorization: Bearer $TOKEN"

# 5. Disable webhook temporarily
curl -X POST http://localhost:8080/api/v1/dags/my-dag/webhook/toggle \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'

Public URL Endpoints ​

A webhook can have a public URL through Dagu Cloud (Dagu v2.20 or later). These endpoints need builtin auth and a server connected to Dagu Console. Changes are admin-only and written to the audit log; developers and managers can read. All take the remoteNode query parameter.

Get Public URL ​

Endpoint: GET /api/v1/dags/{fileName}/webhook/public

Returns the public URL, whether Dagu Cloud holds it, the latest 20 deliveries, and missed counts. Returns 404 when the DAG has no webhook or no public URL.

Response (200):

json
{
  "url": "https://hooks.dagu.sh/7f3c9a1e5b2d4c8e/q3Vt9dXbVQ7m2YkqRk4fJ8sWnP1aLc6tHe0uGz5yBxA",
  "state": "active",
  "maxHooks": 1000,
  "deliveries": [
    {
      "id": "6b1f0c2e-1a5d-4c3e-9f7a-2d8b4e6c1a90",
      "receivedAt": "2026-04-29T11:05:00Z",
      "size": 7421,
      "event": "push",
      "outcome": "started",
      "dagRunId": "6b1f0c2e-1a5d-4c3e-9f7a-2d8b4e6c1a90"
    }
  ],
  "missed": { "expired": 0, "refused": 0 }
}
FieldDescription
urlThe public URL. Absent until Dagu Cloud has given the server its address.
stateactive once Dagu Cloud holds the URL, connecting before then, overLimit when the plan allows fewer public URLs than the server has and this one is left out, or error.
errorWhy the server cannot connect, when state is error.
maxHooksThe most public URLs the server's plan allows, once known.
deliveries[].outcomestarted, waiting (Dagu Cloud sends it again), rejected (signature check failed), skipped (an event the webhook does not take), ignored (the webhook was disabled), or failed.
deliveries[].errorWhy a delivery was rejected, skipped, waiting, or failed.
missedRequests Dagu Cloud dropped since they were last dismissed: expired waited more than 7 days, refused arrived while 1,000 were waiting. since is when the first was counted.

Turn On Public URL ​

Endpoint: POST /api/v1/dags/{fileName}/webhook/public

Gives the webhook a public URL. A webhook that has one keeps it. Admin only.

Response (200): the webhook, with public set.

Error Responses: 400 when the server is not connected to Dagu Cloud or the plan allows no more public URLs, 404 when the DAG has no webhook, 501 when encrypted storage is not available.

Turn Off Public URL ​

Endpoint: DELETE /api/v1/dags/{fileName}/webhook/public

Removes the public URL and its delivery list. Dagu Cloud drops the URL and every request waiting for it. Admin only.

Response (200): the webhook, without public.

Replace Public URL ​

Endpoint: POST /api/v1/dags/{fileName}/webhook/public/regenerate

Gives the webhook a new public URL. The old URL stops working; requests Dagu Cloud already took through it are still delivered. Admin only.

Response (200): the webhook.

Configure Public URL Sender ​

Endpoint: PUT /api/v1/dags/{fileName}/webhook/public/sender

Sets whose signature requests must carry, and which events start runs. Admin only.

Request Body:

json
{
  "sender": "github",
  "secret": "",
  "generateSecret": false,
  "events": ["push", "pull_request.opened"]
}
FieldDescription
sendergithub, stripe, slack, or standard. Omit it to accept anyone with the URL.
secretThe sender's signing secret. Empty keeps the stored secret of the same sender, or, for GitHub, makes one.
generateSecretMake a new GitHub signing secret.
eventsEvents that start runs. Empty takes every event. A name also takes the events under it.

Response (200):

json
{
  "webhook": { "dagName": "my-dag", "public": { "sender": "github", "secretConfigured": true, "events": ["push"], "createdAt": "2026-04-29T11:00:00Z" } },
  "secret": "4f9c2b7e0d1a8c3f5e6b9a2d4c7f1e0b8a3d6c9f2e5b8a1d4c7f0e3b6a9d2c5f"
}

secret is present only when Dagu made one, and is shown only this once.

Error Responses: 400 for an unknown sender, a missing or malformed secret, or more than 50 events.

Dismiss Missed Deliveries ​

Endpoint: DELETE /api/v1/dags/{fileName}/webhook/public/missed

Clears the missed counts. Admin only.

Response (204): No content

Workers Endpoints ​

List Workers ​

Endpoint: GET /api/v1/workers

Retrieves information about connected workers in the distributed execution system.

Query Parameters:

ParameterTypeDescriptionDefault
remoteNodestringRemote node name"local"

Response (200):

json
{
  "workers": [
    {
      "id": "worker-gpu-01",
      "labels": {
        "gpu": "true",
        "cuda": "11.8",
        "memory": "64G",
        "region": "us-east-1"
      },
      "health_status": "WORKER_HEALTH_STATUS_HEALTHY",
      "last_heartbeat": "2024-02-11T12:00:00Z",
      "running_tasks": [
        {
          "dagName": "ml-training-pipeline",
          "dagRunId": "20240211_120000_abc123",
          "rootDagRunName": "ml-training-pipeline",
          "rootDagRunId": "20240211_120000_abc123",
          "parentDagRunName": "",
          "parentDagRunId": "",
          "startedAt": "2024-02-11T12:00:00Z"
        }
      ]
    },
    {
      "id": "worker-cpu-02",
      "labels": {
        "cpu-arch": "amd64",
        "cpu-cores": "32",
        "region": "us-east-1"
      },
      "health_status": "WORKER_HEALTH_STATUS_WARNING",
      "last_heartbeat": "2024-02-11T11:59:50Z",
      "running_tasks": []
    },
    {
      "id": "worker-eu-01",
      "labels": {
        "region": "eu-west-1",
        "compliance": "gdpr"
      },
      "health_status": "WORKER_HEALTH_STATUS_UNHEALTHY",
      "last_heartbeat": "2024-02-11T11:59:30Z",
      "running_tasks": [
        {
          "dagName": "data-processor",
          "dagRunId": "20240211_113000_def456",
          "rootDagRunName": "data-pipeline",
          "rootDagRunId": "20240211_110000_xyz789",
          "parentDagRunName": "data-pipeline",
          "parentDagRunId": "20240211_110000_xyz789",
          "startedAt": "2024-02-11T11:30:00Z"
        }
      ]
    }
  ]
}

Worker Health Status Values:

  • WORKER_HEALTH_STATUS_HEALTHY: Last heartbeat < 5 seconds ago
  • WORKER_HEALTH_STATUS_WARNING: Last heartbeat 5-15 seconds ago
  • WORKER_HEALTH_STATUS_UNHEALTHY: Last heartbeat > 15 seconds ago

Running Task Fields:

  • dagName: Name of the DAG being executed
  • dagRunId: ID of the current DAG run
  • rootDagRunName: Name of the root DAG (for nested workflows)
  • rootDagRunId: ID of the root DAG run
  • parentDagRunName: Name of the immediate parent DAG (empty for root DAGs)
  • parentDagRunId: ID of the immediate parent DAG run
  • startedAt: When the task execution started

Error Response (503) (when coordinator is not running):

json
{
  "code": "service_unavailable",
  "message": "Coordinator service is not available"
}

Wiki Endpoints ​

Wiki endpoints accept optional workspace and remoteNode query parameters. Page paths are relative to the selected workspace and omit the .md extension. All authenticated users can list, read, and search pages they can access. Mutations require permissions.write_dags and a write-capable workspace role.

MethodEndpointDescription
GET/api/v1/wikiList the page tree, or use flat=true for a flat list.
POST/api/v1/wikiCreate a Markdown page.
GET/api/v1/wiki/search?q=...Search page content.
GET/api/v1/wiki/backlinks?path=...List pages that link to a page.
GET/api/v1/wiki/page?path=...Read one page.
PATCH/api/v1/wiki/page?path=...Replace one page's content.
DELETE/api/v1/wiki/page?path=...Delete one page.
POST/api/v1/wiki/page/rename?path=...Rename or move a page or directory.
GET/api/v1/wiki/page/revisions?path=...List saved revisions for one page.
GET/api/v1/wiki/page/revision?path=...Read one saved revision.
POST/api/v1/wiki/page/attachment?path=...Upload an attachment for a page.
GET/api/v1/wiki/page/attachment?path=...Download a page attachment.
POST/api/v1/wiki/delete-batchDelete several pages or directories.

Create a page in the operations directory of the default workspace:

bash
curl -X POST "http://localhost:8080/api/v1/wiki?workspace=default" \
  -H "Content-Type: application/json" \
  -d '{"id":"operations/runbook","content":"# Operations runbook\n"}'

Read or update the same page:

bash
curl "http://localhost:8080/api/v1/wiki/page?workspace=default&path=operations%2Frunbook"

curl -X PATCH "http://localhost:8080/api/v1/wiki/page?workspace=default&path=operations%2Frunbook" \
  -H "Content-Type: application/json" \
  -d '{"content":"# Updated operations runbook\n"}'

Rename and batch-delete requests use newPath and paths respectively:

json
{"newPath":"operations/deploy"}
json
{"paths":["operations/deploy","legacy"]}

The old /api/v1/docs endpoints are not part of the current API. Upgrade direct clients to /api/v1/wiki; Dagu's remote-node proxy falls back to the legacy paths when it connects to an older Dagu node.

See Wiki for Web UI behavior, workspace ownership, storage, and Git Sync.

Calendar Endpoints ​

Calendar endpoints manage the business calendar files in paths.calendars_dir and accept an optional remoteNode query parameter. All authenticated users can list, read, and preview calendars. Creating, updating, and deleting require permissions.write_dags and, where roles apply, the developer role or above; they are refused while Git Sync runs with push_enabled: false.

MethodEndpointDescription
GET/api/v1/calendarsList the built-in calendars and calendar files. includeUsage=true adds expiry warnings, includes, and how many DAGs use each calendar.
POST/api/v1/calendarsCreate a calendar. Without spec, the file starts as Monday to Friday with no holidays, listed through the end of the year.
GET/api/v1/calendars/{name}Read a calendar's content, its problems with positions, and the calendars and DAGs that depend on it.
PUT/api/v1/calendars/{name}Replace a calendar's content. Content with problems is not saved; the problems are returned.
DELETE/api/v1/calendars/{name}Delete a calendar. Returns 409 while a DAG's start schedule uses it, directly or through includes, or another calendar includes it.
POST/api/v1/calendars/{name}/previewApply unsaved content to the dates of a year, marking each date's status and the dates that differ from the saved calendar.

Create a calendar, then add a holiday:

bash
curl -X POST "http://localhost:8080/api/v1/calendars" \
  -H "Content-Type: application/json" \
  -d '{"name":"jp-banking"}'

curl -X PUT "http://localhost:8080/api/v1/calendars/jp-banking" \
  -H "Content-Type: application/json" \
  -d '{"spec":"holidays:\n  - date: 2027-01-11\n    name: Coming of Age Day\n"}'

Preview unsaved content for 2027:

json
{"spec":"holidays: [2027-01-11]\n","year":2027}

The usage counts and dependents list only DAGs in workspaces the caller can see.

Git Sync Endpoints ​

Git Sync manages workflow definitions, Wiki content, calendars, and Git-tracked supporting files. The kind field is one of dag, doc, doc-asset, file, or calendar.

DAG and Wiki page IDs omit their file extension. Supporting-file and attachment IDs retain their complete relative path and file extension. URL-encode IDs in endpoint paths: scripts/report.py becomes scripts%2Freport.py, while assets/100%.txt becomes assets%2F100%25.txt.

All sync endpoints accept an optional remoteNode query parameter.

Get Sync Status ​

Endpoint: GET /api/v1/sync/status

Returns overall sync status, item list, and status counts.

bash
curl "http://localhost:8080/api/v1/sync/status"

Response (200):

json
{
  "enabled": true,
  "repository": "github.com/your-org/dags",
  "branch": "main",
  "summary": "pending",
  "lastSyncAt": "2026-02-26T10:00:00Z",
  "lastSyncCommit": "abc123",
  "items": [
    {
      "itemId": "my-dag",
      "filePath": "my-dag.yaml",
      "displayName": "my-dag.yaml",
      "kind": "dag",
      "status": "modified"
    },
    {
      "itemId": "scripts/report.py",
      "filePath": "scripts/report.py",
      "displayName": "scripts/report.py",
      "kind": "file",
      "status": "synced"
    }
  ],
  "counts": {
    "synced": 1,
    "modified": 1,
    "untracked": 0,
    "conflict": 0,
    "missing": 0
  }
}

Pull from Remote ​

Endpoint: POST /api/v1/sync/pull

Fetches and applies changes from the remote repository. Requires permissions.write_dags.

bash
curl -X POST "http://localhost:8080/api/v1/sync/pull"

Response (200):

json
{
  "success": true,
  "message": "Synced 2 and deleted 1 sync item(s)",
  "synced": ["dag-a", "scripts/report.py"],
  "deleted": ["scripts/obsolete.py"],
  "modified": [],
  "conflicts": [],
  "errors": [],
  "timestamp": "2026-02-26T10:00:00Z"
}

Publish All ​

Endpoint: POST /api/v1/sync/publish-all

Publishes specified items, or all modified/untracked items if itemIds is omitted. Requires permissions.write_dags.

bash
curl -X POST "http://localhost:8080/api/v1/sync/publish-all" \
  -H "Content-Type: application/json" \
  -d '{"message":"Batch publish","itemIds":["my-dag","scripts/report.py"]}'

Request Body:

FieldTypeRequiredDescription
messagestringNoCommit message
itemIdsstring[]NoItem IDs to publish. Omit to publish all modified/untracked.

Response (200): Same shape as Pull response.

Test Connection ​

Endpoint: POST /api/v1/sync/test-connection

Tests repository access and authentication.

bash
curl -X POST "http://localhost:8080/api/v1/sync/test-connection"

Response (200):

json
{
  "success": true,
  "message": "Connection successful"
}

Get Config ​

Endpoint: GET /api/v1/sync/config

bash
curl "http://localhost:8080/api/v1/sync/config"

Response (200):

json
{
  "enabled": true,
  "repository": "github.com/your-org/dags",
  "branch": "main",
  "path": "",
  "pushEnabled": true,
  "auth": {
    "type": "token"
  },
  "autoSync": {
    "enabled": true,
    "onStartup": true,
    "interval": 300
  },
  "commit": {
    "authorName": "Dagu",
    "authorEmail": "dagu@localhost"
  }
}

Update Config ​

Endpoint: PUT /api/v1/sync/config

Admin only.

bash
curl -X PUT "http://localhost:8080/api/v1/sync/config" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "repository": "github.com/your-org/dags",
    "branch": "main",
    "pushEnabled": true,
    "auth": {"type": "token", "token": "ghp_xxx"},
    "autoSync": {"enabled": true, "onStartup": true, "interval": 300},
    "commit": {"authorName": "Dagu", "authorEmail": "dagu@localhost"}
  }'

Get Item Diff ​

Endpoint: GET /api/v1/sync/items/{itemId}/diff

Returns local and remote content for comparison. For binary files, the endpoint reports byte sizes instead of file content. Supporting-file responses can also indicate executable permissions and remote deletion status. Requires the sync service to be configured.

bash
curl "http://localhost:8080/api/v1/sync/items/scripts%2Freport.py/diff"

Response (200):

json
{
  "itemId": "scripts/report.py",
  "filePath": "scripts/report.py",
  "kind": "file",
  "status": "modified",
  "localContent": "print('local')\n",
  "remoteContent": "print('remote')\n",
  "localExecutable": true,
  "remoteExecutable": true
}

For a binary supporting file, binary is true, localSize and remoteSize contain byte counts, and content fields are omitted. A deletion conflict sets remoteDeleted to true and omits remote content.

Response (404): Item not found.

Publish One Item ​

Endpoint: POST /api/v1/sync/items/{itemId}/publish

Requires permissions.write_dags.

bash
curl -X POST "http://localhost:8080/api/v1/sync/items/my-dag/publish" \
  -H "Content-Type: application/json" \
  -d '{"message":"Update dag","force":false}'

Request Body:

FieldTypeRequiredDescription
messagestringNoCommit message
forcebooleanNoForce publish over conflict

Response (200): Same shape as Pull response.

Response (404): Item not found.

Response (409): Conflict detected (when force is false):

json
{
  "itemId": "my-dag",
  "remoteCommit": "abc123",
  "remoteAuthor": "alice",
  "remoteMessage": "Remote update",
  "message": "conflict: \"my-dag\": remote has been updated"
}

Discard Item Changes ​

Endpoint: POST /api/v1/sync/items/{itemId}/discard

Requires permissions.write_dags.

bash
curl -X POST "http://localhost:8080/api/v1/sync/items/my-dag/discard"

Response (200):

json
{
  "message": "Discarded changes"
}

Response (404): Item not found.

Forget Item ​

Endpoint: POST /api/v1/sync/items/{itemId}/forget

Removes the state entry for a missing, untracked, or conflict item. Rejects synced and modified items. Requires permissions.write_dags.

bash
curl -X POST "http://localhost:8080/api/v1/sync/items/missing-dag/forget"

Response (200):

json
{
  "message": "Forgotten"
}

Response (400): Item cannot be forgotten (synced or modified).

Response (404): Item not found.

Delete Item ​

Endpoint: POST /api/v1/sync/items/{itemId}/delete

Removes item from remote repository (git rm + commit + push), local disk, and sync state. Requires permissions.write_dags.

bash
curl -X POST "http://localhost:8080/api/v1/sync/items/my-dag/delete" \
  -H "Content-Type: application/json" \
  -d '{"message":"Remove old DAG","force":false}'

Request Body:

FieldTypeRequiredDescription
messagestringNoCommit message
forcebooleanNoForce delete when item has local modifications

Response (200):

json
{
  "message": "Deleted item: my-dag"
}

Response (400): Item cannot be deleted (untracked, or modified without force, or push disabled).

Response (404): Item not found.

Delete All Missing Items ​

Endpoint: POST /api/v1/sync/delete-missing

Removes all missing items from remote repository, local disk, and sync state. Requires permissions.write_dags. Request body is optional.

bash
curl -X POST "http://localhost:8080/api/v1/sync/delete-missing" \
  -H "Content-Type: application/json" \
  -d '{"message":"Clean up missing items"}'

Request Body:

FieldTypeRequiredDescription
messagestringNoCommit message

Response (200):

json
{
  "deleted": ["missing-a", "missing-b"],
  "message": "Deleted 2 missing item(s)"
}

Response (400): Push is disabled.

Move Item ​

Endpoint: POST /api/v1/sync/items/{itemId}/move

Renames a tracked item locally and in the remote repository. Requires permissions.write_dags.

bash
curl -X POST "http://localhost:8080/api/v1/sync/items/old-dag/move" \
  -H "Content-Type: application/json" \
  -d '{"newItemId":"new-dag","message":"Rename workflow"}'

Request Body:

FieldTypeRequiredDescription
newItemIdstringYesNew item ID
messagestringNoCommit message
forcebooleanNoForce move over conflict

Response (200):

json
{
  "message": "Moved old-dag to new-dag"
}

Response (400): Validation error (cross-kind move, push disabled, invalid ID).

Response (404): Source item not found.

Response (409): Conflict detected (when force is false). Same shape as publish conflict response.

Cleanup ​

Endpoint: POST /api/v1/sync/cleanup

Removes all missing entries from sync state. Does not touch files on disk or remote. Requires permissions.write_dags.

bash
curl -X POST "http://localhost:8080/api/v1/sync/cleanup"

Response (200):

json
{
  "forgotten": ["missing-a", "missing-b"],
  "message": "Cleaned up 2 item(s)"
}

API Versioning ​

  • Current version: v2
  • Legacy v1 endpoints are deprecated but still available
  • Version is included in the URL path: /api/v1/
  • Breaking changes will result in a new API version

Workspace Endpoints ​

Workspace-aware list and search APIs use one optional workspace query parameter:

ValueMeaning
allAll resources the current user or API key can access.
defaultResources without a valid workspace=<name> label.
<workspace>One named workspace.

Mutation APIs use only default or a named workspace; all is an aggregate read value.

List Workspaces ​

Endpoint: GET /api/v1/workspaces

Retrieves workspaces. Requires authentication.

Query Parameters:

ParameterTypeDescriptionDefault
remoteNodestringRemote node name"local"

Response (200):

json
{
  "workspaces": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "name": "production",
      "description": "Production workflows",
      "createdAt": "2026-03-06T10:00:00Z",
      "updatedAt": "2026-03-06T10:00:00Z"
    }
  ]
}

Returns an empty array if no workspaces exist.

Create Workspace ​

Endpoint: POST /api/v1/workspaces

Creates a new workspace. Requires developer role or above.

Request Body:

json
{
  "name": "staging",
  "description": "Optional description"
}

name is required and must match ^[A-Za-z0-9_-]+$. description is optional.

Response (201): The created workspace object.

Error Response (400):

json
{
  "code": "bad_request",
  "message": "Name is required"
}

Error Response (409):

json
{
  "code": "already_exists",
  "message": "Workspace with this name already exists"
}

Get Workspace ​

Endpoint: GET /api/v1/workspaces/{workspaceId}

Retrieves a single workspace by ID. Requires authentication.

Path Parameters:

ParameterTypeDescription
workspaceIdstring (UUID)Workspace ID

Response (200): The workspace object.

Error Response (404): Workspace not found.

Update Workspace ​

Endpoint: PATCH /api/v1/workspaces/{workspaceId}

Updates a workspace. Requires developer role or above. Uses PATCH semantics: only provided fields are updated.

Path Parameters:

ParameterTypeDescription
workspaceIdstring (UUID)Workspace ID

Request Body:

json
{
  "name": "new-name",
  "description": "Updated description"
}

Both fields are optional. Empty string for name is ignored.

Response (200): The updated workspace object.

Error Response (404): Workspace not found.

Error Response (409):

json
{
  "code": "already_exists",
  "message": "Workspace with this name already exists"
}

Delete Workspace ​

Endpoint: DELETE /api/v1/workspaces/{workspaceId}

Deletes a workspace. Requires developer role or above.

Deleting a workspace removes the workspace record only. It does not rewrite DAG files, historical DAG runs, users, or API keys.

Path Parameters:

ParameterTypeDescription
workspaceIdstring (UUID)Workspace ID

Response (204): No content.

Error Response (404): Workspace not found.

Workspace Response Object ​

All endpoints returning workspace data use this shape:

FieldTypeDescription
idstringUUID
namestringWorkspace name
descriptionstringOptional description
createdAtstring (date-time)UTC creation timestamp
updatedAtstring (date-time)UTC last-update timestamp

List Workspace Settings That v3.0 Moves into Projects ​

Endpoint: GET /api/v1/workspaces/deprecation

Lists the workspace settings in use that v3.0 moves into projects. Requires admin role.

Query Parameters:

ParameterTypeDescriptionDefault
remoteNodestringRemote node name"local"

Response (200):

json
{
  "items": [
    { "kind": "baseConfig", "name": "billing" },
    { "kind": "secret", "name": "database/password (workspace billing)" },
    { "kind": "configKey", "name": "auth.oidc.role_mapping.workspace_mappings" },
    { "kind": "user", "name": "alice" }
  ]
}

Items are ordered by kind, then name. kind is one of baseConfig, secret, profileEntry, notificationRoute, incidentPolicy, configKey, user, apiKey, or wikiDirectory. name identifies the setting, such as a workspace, a secret and its workspace, or a username. Returns an empty array when no workspace settings are in use.

See Workspaces for when each kind is reported.

Remote Node Support ​

Most endpoints support the remoteNode query parameter for multi-environment setups:

bash
# Query a remote node
curl "http://localhost:8080/api/v1/dags?remoteNode=production" \
     -H "Authorization: Bearer your-token"

Remote nodes are configured in the server configuration file and allow managing DAGs across multiple Dagu instances from a single interface.

Dagu is open source under the GNU General Public License v3.0.