Back to docs
Overview

Dagy CLI Reference

Complete reference for the `dagy` command-line interface, the primary tool for building, deploying, and operating Dagy workflows.


Installation

Install the Dagy SDK (which includes the CLI) from the project root:

pip install .

For development:

pip install -e ".[dev]"

After installation, the dagy command is available globally. Verify with:

dagy --help

Requirements

  • Python 3.10 or later
  • Core dependencies (installed automatically): boto3, duckdb, pyyaml, tabulate
  • For remote deployment: AWS credentials configured (aws configure or environment variables)

Configuration

Dagy stores all local state under ~/.dagy/ by default. This includes configuration profiles, credentials, run metadata, artifacts, and a DuckDB database for local run history.

Directory Layout

~/.dagy/
├── config.yaml        # Profile configuration
├── credentials        # OAuth access token (chmod 600)
├── dagy.duckdb        # Local run metadata store
├── runs/              # Per-run logs and metadata
│   └── <run_id>/
│       ├── run.log
│       ├── metadata.json
│       └── task_runs/
│           └── <task_run_id>/
│               ├── task.log
│               └── metadata.json
├── artifacts/         # Locally stored task artifacts
└── builds/            # Build output (artifact zips)

Environment Variables

VariableDescriptionDefault
DAGY_LOCAL_DIRBase directory for all local state~/.dagy
DAGY_API_URLAPI URL (overrides profile setting)-
DAGY_APP_URLWeb app URL for OAuth loginhttps://dagy.io
DAGY_PROFILEActive profile name (overrides default_profile in config)-
DAGY_ARTIFACT_BUCKETS3 bucket for artifacts (overrides auto-detection)-
DAGY_LOCAL_VERBOSEEnable verbose logging (true/1/yes/on)false
DAGY_LOCAL_ARTIFACT_MAX_BYTESMaximum artifact size in bytes5242880 (5 MB)
AWS_REGION / AWS_DEFAULT_REGIONAWS region for S3 and STSus-east-1

Profile Management

Profiles allow you to target multiple Dagy environments (dev, staging, production) from a single machine. Profiles are stored in ~/.dagy/config.yaml.

Example config.yaml:

version: 1
default_profile: dev
profiles:
  dev:
    api_url: https://api.dev.dagy.io
    app_url: https://dev.dagy.io
    table_format: heavy_grid
    last_updated: "2026-03-01T12:00:00Z"
  prod:
    api_url: https://api.dagy.io
    table_format: plain
    last_updated: "2026-02-28T09:30:00Z"

Profile resolution precedence:

  1. Explicit --profile <name> flag on the command
  2. DAGY_PROFILE environment variable
  3. default_profile field in config.yaml
  4. First profile alphabetically (fallback)

API URL resolution precedence:

  1. DAGY_API_URL environment variable
  2. api_url field in the resolved profile
  3. None (local-only execution)

Authentication

dagy login

Authenticate via browser-based OAuth. Opens the Dagy web sign-in page and waits for a callback with the access token.

dagy login [--profile <name>]

Flags:

FlagDescription
--profile <name>Use a specific profile's app_url for the login page

Flow:

  1. Starts a temporary HTTP server on 127.0.0.1 (random port).
  2. Opens <app_url>/sign-in?cli_callback=<callback>&state=<csrf_token> in your default browser.
  3. After you sign in, the browser redirects to the local callback with your token.
  4. The token is saved to ~/.dagy/credentials with file permissions 0600.

App URL resolution:

  1. DAGY_APP_URL environment variable
  2. app_url field in the resolved profile
  3. Default: https://dagy.io

Examples:

# Login using defaults
dagy login

# Login against a staging environment
dagy login --profile staging

# Login with explicit app URL
DAGY_APP_URL=https://staging.dagy.io dagy login

Expected output:

Opening browser for login...   (printed to stderr)
Login successful!

Error cases:

ScenarioMessage
Browser cannot be openedPrints the login URL for manual navigation
CSRF state mismatchLogin failed: State mismatch – possible CSRF attack.
Auth provider errorLogin failed: <provider error description>
No callback within 120 secondsLogin failed: Login timed out – no callback received.
User presses Ctrl+CLogin failed: Login cancelled by user.

dagy logout

Remove locally stored credentials.

dagy logout [--profile <name>]

Flags:

FlagDescription
--profile <name>Reserved for future per-profile credential isolation

Example:

dagy logout

Expected output:

Logged out

Note: This command deletes the local ~/.dagy/credentials file. It does not currently revoke the token server-side (see Refactoring Plan §1.7 for the planned improvement).


Commands

dagy config

Interactive configuration wizard for creating, editing, and managing profiles.

dagy config

Behavior:

If no profiles exist, the wizard walks you through creating the first one (profile name, API URL, table format) and sets it as the default.

If profiles exist, a menu is presented:

  1. Select an existing profile → Edit, Set as default, or Delete
  2. Create new profile → Prompts for name, API URL, table format, and default status
  3. Exit → No changes

Validation rules:

  • Profile names: alphanumeric characters and hyphens only (^[A-Za-z0-9-]+$)
  • API URLs: must include http:// or https:// with a valid hostname
  • Localhost URLs: confirmation prompt before accepting

Table format: Profiles can store a preferred table format (used by flows list and other tabular commands). Formats are provided by the tabulate library. All formats are available. The default is heavy_grid.

Corrupt config recovery: If config.yaml is unparseable, it is backed up to config.bak-<timestamp>.yaml and a fresh config is started.

Example session:

$ dagy config
No profiles found. Creating the first profile.
Enter profile name: dev
Enter Dagy API URL: https://api.dev.dagy.io
Select table format:
> Use default (heavy_grid)
  plain
  github
  heavy_grid
  ...
Configuration saved.

dagy build

Build a deployment artifact (zip) from a local flow definition.

dagy build <path>:<flow_fn> [--output-dir <dir>] [--param key=value ...]

Arguments:

ArgumentRequiredDescription
<path>:<flow_fn>YesPath to the Python file and the flow function name, separated by :

Flags:

FlagDescriptionDefault
--output-dir <dir>Directory to write the artifact~/.dagy/builds/
--param key=valueBuild-time parameters (repeatable)-

What happens:

  1. Dynamically loads the Python module at <path> and locates the <flow_fn> function.
  2. Calls flow.build() with any provided parameters to generate a FlowSpec.
  3. Creates an artifact.zip containing flow_spec.json, metadata.json, manifest.json, and the source file.

Artifact contents:

  • flow_spec.json: Serialized DAG (tasks, task runs, edges, outputs)
  • metadata.json: Flow name, version, entrypoint, build time, Python version, dependencies, code_hash
  • manifest.json: File listing within the artifact
  • <source>.py: Copy of the source flow file (if resolvable)

Examples:

# Build from a local file
dagy build examples/flow_example.py:example_flow

# Build with custom output directory
dagy build pipelines/etl.py:daily_etl --output-dir ./dist

# Build with parameters
dagy build flows/main.py:my_flow --param env=prod --param batch_size=100

Expected output:

Artifact: /home/user/.dagy/builds/example_flow/example_flow-20260304T120000-a1b2c3/artifact.zip

Error cases:

ScenarioError
Missing : separatorValueError: Flow target must be in <path>:<flow_fn> format
File not foundFileNotFoundError: Flow file not found: /path/to/file.py
Function not in moduleAttributeError: Flow 'name' not found in /path/to/file.py
Module cannot be loadedImportError: Unable to load module from /path/to/file.py
Invalid param formatValueError: Invalid param 'bad', expected key=value

dagy deploy

Upload a built artifact to S3 and register it with the Dagy API.

dagy deploy <artifact.zip> --deployment <name> --flow-name <name> --flow-version <ver>
    [--bucket <bucket>] [--profile <name>] [--schedule <cron>] [--timezone <tz>]
    [--status <status>] [--default-executor <exec>] [--prefix <prefix>] [--force]

Arguments:

ArgumentRequiredDescription
<artifact.zip>YesPath to the artifact zip (from dagy build)

Flags:

FlagRequiredDescriptionDefault
--deployment <name>YesDeployment name (used to trigger runs)-
--flow-name <name>YesFlow name to register-
--flow-version <ver>YesFlow version to register-
--bucket <bucket>NoS3 bucket for the artifactAuto-detected¹
--profile <name>NoDagy profile for API URLDefault profile
--schedule <cron>NoCron expression for automatic execution-
--timezone <tz>NoIANA timezone for the schedule (e.g. America/New_York, UTC)America/New_York
--status <status>NoDeployment statusACTIVE
--default-executor <exec>NoDefault executor for tasks-
--prefix <prefix>NoS3 key prefixdagy
--execution-mode <mode>NoExecution mode: in-process (single Lambda) or task-isolated (per-task Lambda)in-process
--forceNoDeploy even if code hasn't changed since last deploymentfalse
--dep-packages SLUG [SLUG ...]NoOne or more dependency package slugs to attach-

¹ Bucket auto-detection: If no bucket is specified and DAGY_ARTIFACT_BUCKET is not set, the CLI calls sts:GetCallerIdentity to determine the AWS account ID and constructs dagy-artifacts-<account_id>-<region>. Falls back to dagy-artifacts if STS is unavailable.

S3 key structure: <prefix>/flows/<flow_name>/<flow_version>/artifact.zip

What happens:

  1. Resolves the S3 bucket name.
  2. Change detection: Reads the code_hash from the artifact's metadata.json and compares it against the currently deployed version (fetched via /flows/<name>/latest). If the hashes match, the deploy is skipped with a message. Use --force to override.
  3. Uploads the artifact zip to S3.
  4. If an API URL is configured: validates that the artifact's embedded flow_spec.json matches the provided --flow-name and --flow-version, then registers the flow and creates a deployment via the API.

Examples:

# Deploy to auto-detected bucket and configured API
dagy deploy ./dist/artifact.zip \
  --deployment my-etl \
  --flow-name daily_etl \
  --flow-version 1.0.0

# Deploy with explicit bucket, schedule, and timezone
dagy deploy artifact.zip \
  --deployment hourly-ingest \
  --flow-name ingest \
  --flow-version 2.1.0 \
  --bucket my-company-dagy-artifacts \
  --schedule "0 * * * *" \
  --timezone "America/Los_Angeles"

# Deploy to a specific environment
dagy deploy artifact.zip \
  --deployment staging-pipeline \
  --flow-name pipeline \
  --flow-version 0.5.0 \
  --profile staging

# Deploy with a single dependency package
dagy deploy artifact.zip \
  --deployment my-ml-pipeline \
  --flow-name train_model \
  --dep-packages A1B2C3D4

# Deploy with multiple dependency packages
dagy deploy artifact.zip \
  --deployment my-etl \
  --flow-name daily_etl \
  --dep-packages A1B2C3D4 X9Y8Z7W6

# Deploy with task-isolated execution mode
dagy deploy artifact.zip \
  --deployment my-pipeline \
  --flow-name data_pipeline \
  --flow-version 1.0.0 \
  --execution-mode task-isolated

# Deploy with in-process execution mode and dependency packages
dagy deploy artifact.zip \
  --deployment my-ml-flow \
  --flow-name train_model \
  --flow-version 2.0.0 \
  --execution-mode in-process \
  --dep-packages ML_DEPS_PKG

Expected output:

Deployed to s3://dagy-artifacts-123456789012-useast1/dagy/flows/daily_etl/1.0.0/artifact.zip and registered as daily_etl:1.0.0

Skipped output (no changes):

No code changes detected (code_hash unchanged). Skipping deployment.
  Local:  a1b2c3d4e5f6...
  Remote: a1b2c3d4e5f6...
Use --force to deploy anyway.

Updating settings after deployment:

To change execution mode, schedule, tags, or dependency packages on an existing deployment without redeploying the artifact, use the PUT /deployments/{name}/settings API endpoint or the Flow Settings dialog in the web UI. See the deployment guide for details.

Error cases:

ScenarioError
Artifact file not foundFileNotFoundError: Artifact not found: /path/to/artifact.zip
Flow spec mismatchValueError: Artifact flow spec mismatch. Expected name:version, got other:other
S3 upload failureboto3 exception (permissions, bucket not found)
API registration failureAPIError <status>: <message>

dagy run

Execute a flow locally or trigger a remote run.

dagy run <flow> [--param key=value ...] [--max-workers <n>]
    [--no-fail-fast] [--profile <name>]

Arguments:

ArgumentRequiredDescription
<flow>YesFlow target (see below)

Flow target formats:

  • Local: <path>:<flow_fn> (e.g., examples/flow.py:my_flow), executes locally
  • Remote: <deployment_name> (e.g., my-etl), triggers via API

The CLI determines the mode based on whether an API URL is configured. If DAGY_API_URL is set or the active profile has an api_url, the target is treated as a remote deployment name. Otherwise, it is treated as a local flow reference.

Flags:

FlagDescriptionDefault
--param key=valueParameters to pass to the flow (repeatable)-
--max-workers <n>Maximum parallel task threads (local only)1
--no-fail-fastContinue executing other tasks after a failure (local only)false (fail fast)
--profile <name>Dagy profile for API URL resolutionDefault profile

Local execution details:

  • Uses ThreadPoolExecutor with max_workers threads.
  • Tasks are scheduled based on dependency order; independent tasks run in parallel.
  • On failure with fail_fast=True (default), in-flight tasks are cancelled and a TaskFailedError is raised.
  • Run metadata is stored in DuckDB (~/.dagy/dagy.duckdb).
  • Logs are written to ~/.dagy/runs/<run_id>/run.log.
  • Task artifacts are stored under ~/.dagy/artifacts/<run_id>/<task_run_id>/.
  • Old runs are automatically cleaned up (keeps the last 20 by default).

Remote execution details:

  • Sends a POST to /runs with the deployment name and parameters.
  • Requires a valid authentication token (from dagy login).

Examples:

# Run locally with default settings
dagy run examples/flow_example.py:example_flow

# Run locally with parameters and parallelism
dagy run pipelines/etl.py:daily_etl --param date=2026-03-04 --max-workers 4

# Run locally, continue on failure
dagy run flows/main.py:my_flow --no-fail-fast

# Trigger a remote run
DAGY_API_URL=https://api.dagy.io dagy run my-etl --param env=prod

# Trigger via profile
dagy run my-etl --param env=staging --profile staging

Expected output (local):

Run completed: daily_etl-20260304T120000Z-a1b2c3 status=SUCCEEDED

Expected output (remote):

{'run_id': 'abc-123', 'status': 'PENDING', ...}

Error cases:

ScenarioError
Invalid flow target formatValueError: Flow target must be in <path>:<flow_fn> format
Task failure (fail-fast)TaskFailedError: Task <name> failed: <message>
Dependency cycleDependencyCycleError: Dependency cycle detected or missing dependency output
Task timeoutTaskTimeoutError: Task exceeded timeout of <n>s
API unreachableAPIError 0: Unable to reach Dagy API at <url>. Check DAGY_API_URL/profile and network connectivity.

dagy runs list

List all local run records.

dagy runs list

Output: One line per run showing run ID, status, start time, and flow ID.

Example:

$ dagy runs list
daily_etl-20260304-abc123 SUCCEEDED 2026-03-04T12:00:00Z daily_etl:1.0.0
ingest-20260303-def456 FAILED 2026-03-03T08:00:00Z ingest:2.1.0

Note: This command queries the local DuckDB store. It does not show remote runs.


dagy runs show

Show details for a specific local run, including its task runs.

dagy runs show <run_id>

Arguments:

ArgumentRequiredDescription
<run_id>YesThe run ID (from runs list or run output)

Example:

$ dagy runs show daily_etl-20260304-abc123
Run: daily_etl-20260304-abc123 status=SUCCEEDED flow=daily_etl:1.0.0
Start: 2026-03-04T12:00:00Z End: 2026-03-04T12:05:30Z
- extract-1 extract status=SUCCEEDED attempt=1
- transform-1 transform status=SUCCEEDED attempt=1
- load-1 load status=SUCCEEDED attempt=2

Error cases:

ScenarioOutput
Run not foundRun not found: <run_id>

dagy logs

Display logs for a run or a specific task within a run.

dagy logs <run_id> [--task <task_id>]

Arguments:

ArgumentRequiredDescription
<run_id>YesThe run ID

Flags:

FlagDescription
--task <task_id>Show logs for a specific task run instead of the overall run

Log file locations:

  • Run log: ~/.dagy/runs/<run_id>/run.log
  • Task log: ~/.dagy/runs/<run_id>/task_runs/<task_id>/task.log

Examples:

# View run-level logs
dagy logs daily_etl-20260304-abc123

# View task-specific logs
dagy logs daily_etl-20260304-abc123 --task load-1

Error cases:

ScenarioOutput
Log file not foundLog not found: <path>

dagy flows list

List all deployed flows from the remote API.

dagy flows list [--profile <name>] [--limit <n>]

Flags:

FlagDescriptionDefault
--profile <name>Dagy profile for API URLDefault profile
--limit <n>Page size for API pagination100

Behavior: Automatically paginates through all pages and presents the complete list in a single table. The table format respects the table_format setting in the active profile (default: heavy_grid).

Columns: flow_name, flow_version, created_at, updated_at, artifact_s3_uri

Examples:

# List all flows
dagy flows list

# List flows from staging
dagy flows list --profile staging

Error cases:

ScenarioError
No API URL configuredMissing Dagy API URL. Set DAGY_API_URL or run 'dagy config'.
API unreachableAPIError 0: Unable to reach Dagy API at <url>.
Authentication failureAPIError 401: <message>

HTTP Client

The CLI communicates with the Dagy REST API via DagyClient (dagy.client.http). This client is also available programmatically.

Token resolution:

  1. Explicit token passed to the constructor
  2. access_token from ~/.dagy/credentials
  3. None (unauthenticated)

Default timeout: 10 seconds per request.

User-Agent: dagy-sdk/0.1.0

Error handling: HTTP errors are wrapped in APIError(status_code, message). Network errors (unreachable host, DNS failure, timeout) are wrapped in APIError(status_code=0, message=...).


Error Handling

Exception Hierarchy

Exception
├── DagyError
│   ├── APIError(status_code, message)
│   ├── ParameterTypeError
│   └── RetryConfigurationError
└── LocalExecutionError (RuntimeError)
    ├── DependencyCycleError
    ├── TaskFailedError
    ├── ArtifactSerializationError
    └── TaskTimeoutError

CLI Error Patterns

The CLI uses SystemExit for user-facing errors. These produce a non-zero exit code and a message on stderr/stdout without a Python traceback.

For unexpected exceptions (bugs, network issues), the full traceback is shown. This behavior should be improved (see Refactoring Plan §1.3) to provide structured exit codes.


Credential Security

  • Credentials are stored at ~/.dagy/credentials as JSON.
  • File permissions are set to 0600 (owner read/write only).
  • The CSRF state token for OAuth uses secrets.token_urlsafe(32).
  • The OAuth callback server binds only to 127.0.0.1 (not 0.0.0.0).
  • The config file (~/.dagy/config.yaml) is also set to 0600.

Local Execution Architecture

When running locally (dagy run <path>:<flow_fn>), the execution follows this pipeline:

  1. Load: Dynamically imports the Python module and locates the @flow-decorated function.
  2. Build: Calls flow.build() to generate a FlowSpec (DAG of tasks, edges, and serialized outputs).
  3. Store: Creates a run record in the local DuckDB database.
  4. Execute: Schedules tasks in dependency order using ThreadPoolExecutor. Each task:
    • Resolves inputs by hydrating $ref references from upstream task outputs.
    • Executes the task function with configurable retries, delays (with jitter), and timeouts.
    • Stores the result as a local artifact.
  5. Finalize: Updates the run status, writes metadata JSON, and cleans up old runs.

Quick Reference

dagy login [--profile <name>]           # Authenticate via browser OAuth
dagy logout [--profile <name>]          # Remove local credentials
dagy config                             # Interactive profile management
dagy build <path:fn> [flags]            # Build artifact zip
dagy deploy <artifact> [flags]          # Upload to S3 and register (supports --dep-packages)
dagy run <target> [flags]               # Execute locally or trigger remote
dagy runs list                          # List local runs
dagy runs show <run_id>                 # Show run details
dagy logs <run_id> [--task <id>]        # View run/task logs
dagy flows list [--profile] [--limit]   # List remote flows