Plans API
Plans are the review and deployment mechanism for schema changes. When dagctl detects new commits in your repository, it triggers an explain step that shows what will change. A human then approves or rejects that plan. Approval triggers an execute step that applies the changes.
Both SQLMesh and dbt projects use the same plan endpoints. The internal structure of a plan object differs slightly between the two (see response examples below).
Plan Lifecycle
running (explain) -> pending (awaiting approval) -> approved
|
running (execute) -> completed
-> failed -> (retry)
rejected / cancelled
A plan that is auto-discarded because the repository changed since the explain was created will have status: "rejected" with auto_discarded: true in the plan_output.
GET /api/v1/projects/:id/plans
Returns a paginated list of plans for a project.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id |
string | Project ID |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
integer | 1 | Page number |
limit |
integer | 25 | Results per page |
status |
string | - | Filter by status: pending, running, completed, failed, rejected, cancelled |
Response
{
"plans": [
{
"id": "plan_abc123",
"plan_id": "plangroup_xyz",
"plan_type": "explain",
"project_id": "proj_xyz",
"from_commit_sha": "a1b2c3d",
"to_commit_sha": "f6e5d4c",
"commit_message": "Add orders model",
"commits_behind": 1,
"status": "pending",
"triggered_by": "manual",
"created_at": "2026-04-28T10:00:00Z",
"updated_at": "2026-04-28T10:02:30Z"
}
],
"total": 42,
"page": 1,
"limit": 25,
"total_pages": 2
}
GET /api/v1/projects/:id/plans/pending
Returns the count of plans currently awaiting approval.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id |
string | Project ID |
Response
POST /api/v1/projects/:id/plans
Manually triggers a new explain plan for a project. dagctl resolves the current branch HEAD to a commit SHA, creates an explain record, and launches a Kubernetes job to compute the diff.
If a plan already exists for the same commit, the existing plan is returned instead of creating a duplicate.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id |
string | Project ID |
Request Body
No body required. The plan is always created against the current branch HEAD.
Response
{
"message": "Plan created and explain job triggered",
"plan_id": "plan_abc123",
"plan_group_id": "plangroup_xyz",
"job_name": "analytics-plan-explain-20260428-100000",
"status": "running",
"initial_status": "running"
}
If the commit is already deployed:
{
"message": "Commit a1b2c3d is already deployed",
"status": "already_deployed",
"to_commit_sha": "a1b2c3d"
}
If a plan already exists for the commit:
{
"message": "A plan already exists for commit a1b2c3d",
"status": "existing_plan",
"plan_id": "plan_abc123",
"plan_group_id": "plangroup_xyz",
"current_status": "pending"
}
GET /api/v1/plans/:planId
Returns a single plan by ID. Works for both SQLMesh and dbt plan records.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
planId |
string | Plan ID |
Response (SQLMesh plan)
{
"id": "plan_abc123",
"plan_id": "plangroup_xyz",
"plan_type": "explain",
"project_id": "proj_xyz",
"from_commit_sha": "a1b2c3d",
"to_commit_sha": "f6e5d4c",
"commit_message": "Add orders model",
"commits_behind": 1,
"status": "pending",
"job_name": "analytics-plan-explain-20260428-100000",
"plan_output": {
"environment": "prod",
"is_new_environment": false,
"modelsAdded": 1,
"modelsModified": 2,
"modelsRemoved": 0,
"directly_modified": ["orders.stg_orders"],
"indirectly_modified": ["orders.fct_orders"],
"modelsNeedingBackfill": 1,
"explain_completed": true
},
"triggered_by": "manual",
"created_at": "2026-04-28T10:00:00Z",
"updated_at": "2026-04-28T10:02:30Z"
}
GET /api/v1/plans/group/:planGroupId/steps
Returns all steps (explain and execute) for a plan group. Use this to show the full plan timeline.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
planGroupId |
string | The shared plan_id field that links explain and execute steps |
Response
{
"plan_group_id": "plangroup_xyz",
"project_id": "proj_xyz",
"steps": [
{
"id": "plan_abc123",
"plan_type": "explain",
"status": "approved",
...
},
{
"id": "plan_def456",
"plan_type": "execute",
"status": "completed",
...
}
],
"current_status": "completed"
}
For running plans, logs in each step is populated with live output from Kubernetes rather than the stored database value.
GET /api/v1/plans/:planId/logs
Returns logs for a plan step. For running plans, logs are fetched live from Kubernetes. For completed or failed plans, stored logs are returned.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
planId |
string | Plan ID |
Response
{
"planId": "plan_abc123",
"logs": "...",
"status": "completed",
"jobName": "analytics-plan-execute-20260428-100500",
"podName": "analytics-plan-execute-20260428-100500-xk9vz"
}
POST /api/v1/plans/:planId/approve
Approves a pending explain plan and launches the execute step. Before approving, dagctl re-validates that the repository HEAD has not changed since the explain was created. If it has, the plan is auto-discarded and the response will indicate auto_discarded: true.
Requires the approve_plan permission.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
planId |
string | Plan ID (must be an explain step in pending status) |
Response
{
"message": "Plan approved and execution started",
"explain_plan_id": "plan_abc123",
"execution_plan_id": "plan_def456",
"plan_group_id": "plangroup_xyz",
"job_name": "analytics-plan-execute-20260428-100500"
}
If the repository changed since the explain was created:
{
"message": "Plan auto-discarded due to repository changes",
"plan_id": "plan_abc123",
"status": "auto_discarded",
"auto_discarded": true,
"expected_commit": "f6e5d4c",
"current_commit": "9z8y7x6",
"reason": "Repository has new commits. Please create a new plan to review the latest changes."
}
POST /api/v1/plans/:planId/reject
Rejects a pending plan. The plan is marked as rejected and no execution occurs.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
planId |
string | Plan ID (must be in pending status) |
Response
POST /api/v1/plans/:planId/cancel
Cancels a running or approved plan. The Kubernetes job is deleted and the plan is marked as cancelled. Partial logs captured at the time of cancellation are stored.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
planId |
string | Plan ID (must be in running or approved status) |
Response
POST /api/v1/plans/:planId/retry
Retries a failed or cancelled execution plan. The commit SHA is re-validated against the current branch HEAD before the retry proceeds. Maximum 3 retries per plan group (4 total executions).
Path Parameters
| Parameter | Type | Description |
|---|---|---|
planId |
string | Plan ID (must be an execute step in failed or cancelled status) |
Response
DELETE /api/v1/plans/:planId
Deletes a pending plan. Only plans in pending status can be deleted.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
planId |
string | Plan ID |