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 configureor 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
| Variable | Description | Default |
|---|---|---|
DAGY_LOCAL_DIR | Base directory for all local state | ~/.dagy |
DAGY_API_URL | API URL (overrides profile setting) | - |
DAGY_APP_URL | Web app URL for OAuth login | https://dagy.io |
DAGY_PROFILE | Active profile name (overrides default_profile in config) | - |
DAGY_ARTIFACT_BUCKET | S3 bucket for artifacts (overrides auto-detection) | - |
DAGY_LOCAL_VERBOSE | Enable verbose logging (true/1/yes/on) | false |
DAGY_LOCAL_ARTIFACT_MAX_BYTES | Maximum artifact size in bytes | 5242880 (5 MB) |
AWS_REGION / AWS_DEFAULT_REGION | AWS region for S3 and STS | us-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:
- Explicit
--profile <name>flag on the command DAGY_PROFILEenvironment variabledefault_profilefield inconfig.yaml- First profile alphabetically (fallback)
API URL resolution precedence:
DAGY_API_URLenvironment variableapi_urlfield in the resolved profileNone(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:
| Flag | Description |
|---|---|
--profile <name> | Use a specific profile's app_url for the login page |
Flow:
- Starts a temporary HTTP server on
127.0.0.1(random port). - Opens
<app_url>/sign-in?cli_callback=<callback>&state=<csrf_token>in your default browser. - After you sign in, the browser redirects to the local callback with your token.
- The token is saved to
~/.dagy/credentialswith file permissions0600.
App URL resolution:
DAGY_APP_URLenvironment variableapp_urlfield in the resolved profile- 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:
| Scenario | Message |
|---|---|
| Browser cannot be opened | Prints the login URL for manual navigation |
| CSRF state mismatch | Login failed: State mismatch – possible CSRF attack. |
| Auth provider error | Login failed: <provider error description> |
| No callback within 120 seconds | Login failed: Login timed out – no callback received. |
| User presses Ctrl+C | Login failed: Login cancelled by user. |
dagy logout
Remove locally stored credentials.
dagy logout [--profile <name>]
Flags:
| Flag | Description |
|---|---|
--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:
- Select an existing profile → Edit, Set as default, or Delete
- Create new profile → Prompts for name, API URL, table format, and default status
- Exit → No changes
Validation rules:
- Profile names: alphanumeric characters and hyphens only (
^[A-Za-z0-9-]+$) - API URLs: must include
http://orhttps://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:
| Argument | Required | Description |
|---|---|---|
<path>:<flow_fn> | Yes | Path to the Python file and the flow function name, separated by : |
Flags:
| Flag | Description | Default |
|---|---|---|
--output-dir <dir> | Directory to write the artifact | ~/.dagy/builds/ |
--param key=value | Build-time parameters (repeatable) | - |
What happens:
- Dynamically loads the Python module at
<path>and locates the<flow_fn>function. - Calls
flow.build()with any provided parameters to generate aFlowSpec. - Creates an
artifact.zipcontainingflow_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_hashmanifest.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:
| Scenario | Error |
|---|---|
Missing : separator | ValueError: Flow target must be in <path>:<flow_fn> format |
| File not found | FileNotFoundError: Flow file not found: /path/to/file.py |
| Function not in module | AttributeError: Flow 'name' not found in /path/to/file.py |
| Module cannot be loaded | ImportError: Unable to load module from /path/to/file.py |
| Invalid param format | ValueError: 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:
| Argument | Required | Description |
|---|---|---|
<artifact.zip> | Yes | Path to the artifact zip (from dagy build) |
Flags:
| Flag | Required | Description | Default |
|---|---|---|---|
--deployment <name> | Yes | Deployment name (used to trigger runs) | - |
--flow-name <name> | Yes | Flow name to register | - |
--flow-version <ver> | Yes | Flow version to register | - |
--bucket <bucket> | No | S3 bucket for the artifact | Auto-detected¹ |
--profile <name> | No | Dagy profile for API URL | Default profile |
--schedule <cron> | No | Cron expression for automatic execution | - |
--timezone <tz> | No | IANA timezone for the schedule (e.g. America/New_York, UTC) | America/New_York |
--status <status> | No | Deployment status | ACTIVE |
--default-executor <exec> | No | Default executor for tasks | - |
--prefix <prefix> | No | S3 key prefix | dagy |
--execution-mode <mode> | No | Execution mode: in-process (single Lambda) or task-isolated (per-task Lambda) | in-process |
--force | No | Deploy even if code hasn't changed since last deployment | false |
--dep-packages SLUG [SLUG ...] | No | One 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:
- Resolves the S3 bucket name.
- Change detection: Reads the
code_hashfrom the artifact'smetadata.jsonand compares it against the currently deployed version (fetched via/flows/<name>/latest). If the hashes match, the deploy is skipped with a message. Use--forceto override. - Uploads the artifact zip to S3.
- If an API URL is configured: validates that the artifact's embedded
flow_spec.jsonmatches the provided--flow-nameand--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:
| Scenario | Error |
|---|---|
| Artifact file not found | FileNotFoundError: Artifact not found: /path/to/artifact.zip |
| Flow spec mismatch | ValueError: Artifact flow spec mismatch. Expected name:version, got other:other |
| S3 upload failure | boto3 exception (permissions, bucket not found) |
| API registration failure | APIError <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:
| Argument | Required | Description |
|---|---|---|
<flow> | Yes | Flow 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:
| Flag | Description | Default |
|---|---|---|
--param key=value | Parameters to pass to the flow (repeatable) | - |
--max-workers <n> | Maximum parallel task threads (local only) | 1 |
--no-fail-fast | Continue executing other tasks after a failure (local only) | false (fail fast) |
--profile <name> | Dagy profile for API URL resolution | Default profile |
Local execution details:
- Uses
ThreadPoolExecutorwithmax_workersthreads. - 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 aTaskFailedErroris 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
/runswith 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:
| Scenario | Error |
|---|---|
| Invalid flow target format | ValueError: Flow target must be in <path>:<flow_fn> format |
| Task failure (fail-fast) | TaskFailedError: Task <name> failed: <message> |
| Dependency cycle | DependencyCycleError: Dependency cycle detected or missing dependency output |
| Task timeout | TaskTimeoutError: Task exceeded timeout of <n>s |
| API unreachable | APIError 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:
| Argument | Required | Description |
|---|---|---|
<run_id> | Yes | The 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:
| Scenario | Output |
|---|---|
| Run not found | Run 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:
| Argument | Required | Description |
|---|---|---|
<run_id> | Yes | The run ID |
Flags:
| Flag | Description |
|---|---|
--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:
| Scenario | Output |
|---|---|
| Log file not found | Log not found: <path> |
dagy flows list
List all deployed flows from the remote API.
dagy flows list [--profile <name>] [--limit <n>]
Flags:
| Flag | Description | Default |
|---|---|---|
--profile <name> | Dagy profile for API URL | Default profile |
--limit <n> | Page size for API pagination | 100 |
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:
| Scenario | Error |
|---|---|
| No API URL configured | Missing Dagy API URL. Set DAGY_API_URL or run 'dagy config'. |
| API unreachable | APIError 0: Unable to reach Dagy API at <url>. |
| Authentication failure | APIError 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:
- Explicit
tokenpassed to the constructor access_tokenfrom~/.dagy/credentialsNone(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/credentialsas 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(not0.0.0.0). - The config file (
~/.dagy/config.yaml) is also set to0600.
Local Execution Architecture
When running locally (dagy run <path>:<flow_fn>), the execution follows this pipeline:
- Load: Dynamically imports the Python module and locates the
@flow-decorated function. - Build: Calls
flow.build()to generate aFlowSpec(DAG of tasks, edges, and serialized outputs). - Store: Creates a run record in the local DuckDB database.
- Execute: Schedules tasks in dependency order using
ThreadPoolExecutor. Each task:- Resolves inputs by hydrating
$refreferences from upstream task outputs. - Executes the task function with configurable retries, delays (with jitter), and timeouts.
- Stores the result as a local artifact.
- Resolves inputs by hydrating
- 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