Skip to content

Public API

The public API lets a script do what you can already do in dagweave: read your runs, a single run in full with its steps, one step’s log output, your workflow list and your team’s audit trail; look up the node types a workflow may place and check a draft before you save it; save a new one, edit it, relabel it or delete it; and start a run on one of your clusters. Use it to put run status on a dashboard, feed a chatbot, gate a deploy on a validation pass, keep a workspace in step with workflows generated elsewhere, or fire a workflow from whatever already fires the rest of your jobs.

Eight of the fourteen endpoints only read. The other six start runs or change what is stored.

Requests carry an API token instead of a session cookie. The base URL is https://api.dagweave.com and every route sits under /api/v1.

Each /api/v1 route hands straight off to the function that already serves the dagweave UI. The token layer swaps the session cookie for a bearer token, charges the rate limit, and checks one scope. It never decides what you may read.

Two things follow from that. A token can never see more than the person who minted it sees in the product. And when a plan gate goes on a screen, it applies to the API the same day, because it is the same check running in the same place.

Send the token as a bearer credential:

Terminal window
curl -H "Authorization: Bearer $DAGWEAVE_TOKEN" \
https://api.dagweave.com/api/v1/runs

An unknown token, an expired token, and a revoked token all come back as the same 401, reading invalid or revoked API token, so guessing at tokens tells you nothing. A missing header is also a 401, reading unauthorized. See API tokens for how to mint one.

A token carries one or more scopes. Each endpoint requires exactly one.

  • runs:read for the run list and the single-run read.
  • runs:logs for one step’s log output.
  • runs:write for starting a run and for rerunning one.
  • workflows:read for the workflow list, the node types, and the draft check.
  • workflows:write for saving a workflow, editing one, and setting its labels.
  • workflows:delete for removing one.
  • audit:read for the audit trail.

No scope implies another. runs:write does not imply runs:read: a token that fires a workflow cannot read the history of what anyone else fired, and a token that reads runs cannot start one, so the POST comes back 403. runs:logs is not implied by runs:read either. A run row is a name, a phase and a duration; a step’s logs are whatever it was holding when it died, which is usually more than the row. Ask for both if you want both, because a token minted with runs:read alone gets a 403 at the log route.

Tokens minted before runs:logs existed already carry it. Nothing you have is about to stop working; it is the next token you mint that has to name the scope.

There is no wildcard. A scope only ever narrows: audit:read on a plan without the audit log still gets you a 402, and a viewer’s token still reads only what a viewer reads. A viewer’s token carrying runs:write still cannot start a run, because the permission check behind the endpoint is the one the UI runs.

Requires runs:read. Returns your team’s runs, newest first, as a JSON array.

  • limit sets the page size. It defaults to 50 and is capped at 200.
  • offset skips that many rows.
  • includeTest=true includes runs launched from the builder’s test panel. They are hidden by default.

Each row carries id, workflowId, workflowName, connectorId, triggerId, source, actorId, phase, message, startedAt, finishedAt, createdAt, and the argoNamespace and argoName the run took on the cluster. A failed run also carries rootCauseNode and rootCauseMessage, which name the step that broke it. Optional fields are dropped when empty; startedAt and finishedAt are null until the run reaches that point.

Runs you did not start are not in this list, and nothing on this page lists them. The single-run read still answers for one if you already have its id.

Requires runs:read. Returns one run in full. An id that is not yours is a 404, the same answer as an id that does not exist.

The body carries id, workflowId, connectorId, triggerId, source, phase, message, progress, startedAt, finishedAt, argoNamespace, argoName, and three arrays:

  • nodes, one per step, each with id, name, phase, message, and its own startedAt and finishedAt. A step that declared them also carries a resources block holding requests and limits, each with cpu and memory as the Kubernetes quantity strings you wrote, such as 100m and 128Mi. containerStartedAt is when the container itself began, which is later than startedAt by however long the step queued. A step that waited on something carries wait with the kind it waited for, and a step on a traced cluster carries segments, the named parts of its time in milliseconds.
  • artifacts, each with name, bucket, key, and url.
  • logs, each with nodeId and location. The location points at your own storage, so the log bytes do not pass through dagweave.

Requires runs:logs. runs:read is not enough, and a token carrying only that gets a 403 here. Returns one step’s own log output. Both ids come from the reads above: the run id, and the id of a node inside that run’s nodes array. Since neither read is open to a token holding runs:logs on its own, mint the two scopes together. A run that is not yours is a 404, the same answer as one that does not exist.

  • tail caps the trailing lines returned. It defaults to 200 and is capped at 5000.
  • since is a Go duration such as 30m or 2h, narrowing the range read. It is capped at 720h, and anything unparseable is ignored rather than refused.
  • containers=all adds the init and wait sidecars. The default is the step’s own container, which is what you almost always want.

The body carries kind and nodeId, plus one of location, content or message depending on the kind:

  • pointer puts the log object’s address in location. dagweave does not hold your object-store credentials, so it hands you the pointer and you fetch the bytes yourself.
  • content puts the lines themselves in content, served from storage dagweave does hold.
  • collecting means lines are expected for this step but have not landed yet. Ask again shortly.
  • expired and none mean there is nothing to return, and message says which of the two it is.

A log backend that fails is a 502, because that is not your request’s fault.

Requires workflows:read. Returns your team’s workflows as a JSON array of id and name, plus folder, namespace, and labels where they are set.

  • q matches a substring of the name, case-insensitively.
  • label, namespace, and folder are exact-match facets.
  • limit and offset page the filtered list. Send neither and you get all of it.

Filtering happens before paging, so a page is a slice of what matched. X-Total-Count carries the filtered total on every response, which is enough to size a pager without walking every page.

Requires workflows:read. Returns the node types a dagweave IR document may place, so a draft is written from field names that exist rather than invented ones. Read it before you write IR JSON: this is what stops a fault being written, where the draft check below finds one afterwards.

  • typeId narrows the answer to one type. Omit it for the whole list.

The answer is a JSON array sorted by typeId, and an array of one when you named a type. Each row carries typeId, label, description, version, core, podBased, paletteHidden and ports. ports holds an inputs list and an outputs list, and every port in them has a name and a type of parameter or artifact. typeId is the value a node’s own typeId field has to carry, and version is what its nodeVersion has to quote to pin the type.

configSchema is the one field the list leaves off every row. Name a typeId and it comes back on that single row: the JSON Schema for that type’s config, which is where the fields a draft has to get right are written down. That is the reason to call this before writing IR rather than after.

core false means a plan without the full node set refuses to save a workflow placing that type. podBased false means the step runs on the workflow’s agent or nowhere, so the pod-level settings a node can carry reach nothing. paletteHidden marks a type that exists so a foreign Argo manifest imports faithfully; writing one is valid, but a draft started from nothing usually wants the opinionated type it mirrors, and Import-only types names the counterpart for each.

A typeId that names nothing is a 404 whose message lists every id that does exist, so a misspelling costs one call rather than another guess.

The registry is the same for every team, so the answer does not depend on who asks. Nothing you own is read and nothing is stored. The catalog is not in this list: a catalog item is not a node type, and the node that references one is filled in when a workflow is saved.

Requires workflows:read. Checks a candidate workflow and tells you what is wrong with it. Nothing is stored and nothing is dispatched, so the same bytes always give the same answer and you can call it as often as you like while drafting. It is a POST because it carries a document, not because it changes anything.

The body is one field:

{"workflow": "apiVersion: argoproj.io/v1alpha1\nkind: Workflow\n..."}

Send the whole document as a single string, either an Argo YAML manifest (Workflow, WorkflowTemplate, ClusterWorkflowTemplate or CronWorkflow) or dagweave IR JSON. The shape is detected, so you never declare which you sent. Any other field in the body is a 400, and so is an empty workflow.

A document that will not run is still a 200. The faults are the answer, not an error:

{
"valid": false,
"format": "argo-yaml",
"faults": [
{
"severity": "error",
"rule": "template-not-found",
"path": "spec.templates[0].dag.tasks[1].template",
"message": "spec.templates[0].dag.tasks[1].template names template \"trasnform\", which this workflow does not define. This workflow defines: main, extract."
}
],
"manifest": "apiVersion: argoproj.io/v1alpha1\nkind: Workflow\n..."
}

format echoes how your input was read, as argo-yaml or dagweave-ir. valid is false when any fault is an error; a warning means it would run but something in it is dead. rule is the machine-readable half of a fault, one of parse, schema, unknown-field, entrypoint-unset, template-not-found, task-not-found, unused-parameter, import, compile or not-runnable. path locates the fault in the document you sent, dotted for a manifest and as a JSON pointer for IR, and is absent when nothing narrower than the whole document is to blame. manifest is what your draft compiles to, so you can see what it becomes before you save it. It is absent when the draft did not compile.

Requires runs:write. Starts a run of a stored workflow on one of your connected clusters and returns the new run’s id. It launches real work on a real cluster and cannot be called back.

Terminal window
curl -X POST https://api.dagweave.com/api/v1/workflows/wf-04c9/run \
-H "Authorization: Bearer $DAGWEAVE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"connectorId":"con-1a77","namespace":"argo"}'
  • connectorId is required and names the connector to dispatch to.
  • namespace is required and names the Kubernetes namespace on that connector’s cluster.
  • argoInstanceId picks the instance when the connector serves more than one.
  • params is an object of string values merged into the workflow’s parameters at dispatch, so an ad-hoc run can be parameterised without editing the workflow.

The body is an allowlist: any other field is a 400 rather than being quietly dropped. The builder’s test-run flag is deliberately not on it, so a token cannot fire the kind of run that skips the checks a real one gets.

The reply is a 200 carrying the new run’s id:

{"runId": "run-8f21"}

Feed that straight into GET /api/v1/runs/{id} to follow it.

Every write on this page needs one, and a call without it is a 400 that changes nothing. A client whose response was lost cannot tell a request that never landed from one that started a run and then dropped the reply, so retrying is the only thing it can do. The key turns that retry into a read of what the first attempt answered.

  • Repeat the key and you get the first call’s reply back with Idempotency-Replayed: true on it. The write does not happen a second time, and the audit log holds one record rather than two.
  • Reuse the key for a different request and you get a 409. Answering that with the first run’s id would hide the bug.
  • Send it again while the first call is still in flight and you also get a 409. Retry it.
  • A key is remembered for 24 hours.
  • A refused call releases its key, because nothing started. Fix what was wrong and retry under the same key.

Keys are at most 255 characters. A UUID is the obvious thing to send.

A replay still spends a rate-limit unit, so it is never the cheap path.

Requires runs:write. Runs again what a past run ran: the same workflow, on the same connector, in the same namespace, with the same runtime parameters. Like starting a run it launches real work on a real cluster and cannot be called back.

Terminal window
curl -X POST https://api.dagweave.com/api/v1/runs/run-8f21/rerun \
-H "Authorization: Bearer $DAGWEAVE_TOKEN" \
-H "Idempotency-Key: $(uuidgen)"

There is no body. A rerun cannot pick the connector, the namespace or the parameters, because it reuses the ones the original run used. That is what makes it narrower than POST /api/v1/workflows/{id}/run rather than a second spelling of it, and it is why the same scope opens both.

The reply is a 200 carrying the new run’s id, in the shape a run answers with:

{"runId": "run-b30e"}

The new run is recorded as a manual run fired by the token’s owner, whatever fired the original. A run id that is not yours is a 404, the same answer as one that does not exist.

One rerun is refused. A run started from the builder’s test panel comes back 400, dispatches nothing, and is recorded as a denied workflow.run. A rerun of a test stays a test, and a test run is exempt from the monthly execution quota, so over a token that can be minted to last two years, or never to expire, that one rerun would be unmetered work on your cluster. Rerun it from the builder if a test is what you want.

Requires workflows:write. Saves a new workflow and returns its id and the version string the next update has to quote. It stores a definition and starts nothing; the workflow runs when somebody dispatches it.

Terminal window
curl -X POST https://api.dagweave.com/api/v1/workflows \
-H "Authorization: Bearer $DAGWEAVE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"workflow":"apiVersion: argoproj.io/v1alpha1\nkind: Workflow\nmetadata:\n generateName: hello-\nspec:\n entrypoint: main\n templates:\n - name: main\n container:\n image: alpine:3.19\n command: [echo, hi]\n"}'

The body is the one workflow field the draft check takes, in the same two formats: an Argo YAML manifest or dagweave IR JSON, detected rather than declared. Any other field is a 400, and so is an empty workflow. So is baseVersion, which belongs on an update, because a create has nothing to be based on.

The reply is a 200:

{"id": "wf-04c9", "version": "9f86d081884c7d65...2b0061"}

version is a long hex digest, shortened here. Treat it as opaque: quote it back on the next update rather than building one. It fingerprints the graph that was stored rather than the bytes you sent, which matters because a save rewrites catalog references on the way in.

The draft goes through the very checks POST /api/v1/workflows/validate runs. A draft carrying any error-severity fault is refused with a 422 whose body is that endpoint’s own answer, faults and all, and nothing is stored. Warnings do not refuse: a parameter nothing reads is worth knowing about and is not worth blocking a save over. Check a draft there and fix what comes back, rather than saving and reading the refusal.

What lands in the store is the graph the checks ran against, not the document you sent, so a workflow saved as Argo YAML reads back as dagweave IR.

A create runs the same admission gates a canvas save runs, so a token is held to the limits a person is:

  • An unverified account is a 403, and that one body carries a code field reading email_unverified next to the message.
  • Over your plan’s workflow count is a 429. It carries no Retry-After, because it is not the rate limit.
  • A node type or a catalog template your plan does not include is a 402.
  • A catalog reference that resolves to nothing is a 400.

Requires workflows:write. Replaces a stored workflow with a new whole document and returns its new version.

Terminal window
curl -X PUT https://api.dagweave.com/api/v1/workflows/wf-04c9 \
-H "Authorization: Bearer $DAGWEAVE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"workflow":"apiVersion: argoproj.io/v1alpha1\nkind: Workflow\nmetadata:\n generateName: hello-\nspec:\n entrypoint: main\n templates:\n - name: main\n container:\n image: alpine:3.19\n command: [echo, goodbye]\n","baseVersion":"9f86d081884c7d65...2b0061"}'

This is a replacement, not a patch. Whatever you leave out of the document is gone from the workflow.

baseVersion is the version the last save of this workflow returned, and it is required: a body without it is a 400 and changes nothing. Quote a version that is no longer the stored one and you get a 409, the workflow does not move, and the refusal is recorded as a denied workflow.update. Read the workflow again, re-apply your edit on top of what you read, and quote the version you just read. There is no merge, because an update replaces the whole document and a merge would be dagweave guessing at intent on two graphs it did not author.

The reply is a 200 in the shape a create answers with, carrying the id you addressed and the version that replaced the one you quoted:

{"id": "wf-04c9", "version": "3a7bd3e2360a3d29...5f8e01"}

The document is checked exactly as a create’s is, and a 422 leaves what is stored untouched.

A 404 covers three cases that are one case from outside: the id does not exist, it belongs to another team, or it is one of the examples dagweave seeds. Those examples are listed to everyone by GET /api/v1/workflows and owned by nobody, so a save against one is refused whatever version you quote, exactly as it is on the canvas. Duplicate it in the UI and edit the copy.

A token carrying only workflows:write cannot go and look a version up, because writing is not a way to read. Either it created the workflow and kept what the save returned, or whoever did handed the version over.

Requires workflows:write. Replaces the labels on a stored workflow. Labels are how workflows are grouped and filtered, and GET /api/v1/workflows takes one as a facet.

Terminal window
curl -X PUT https://api.dagweave.com/api/v1/workflows/wf-04c9/labels \
-H "Authorization: Bearer $DAGWEAVE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"labels":["prod","etl"]}'

Success is a 200 with an empty body. The definition does not change and the workflow’s version does not move, so relabelling never invalidates a baseVersion somebody is holding.

labels is the whole set the workflow should end up with, so carry over the ones it already has unless you mean to drop them. Send {"labels": []} to clear them all.

The field is required, and {} is a 400 rather than a clear. The set is replaced wholesale, so a body that forgot to mention labels would strip every one the workflow had and get a 200 for it. Any field the endpoint does not declare is a 400 too.

A 404 covers the same three cases an update’s does. The audit trail records how many labels landed and never which, because a label is your own text.

Requires workflows:delete. That is a scope of its own and workflows:write does not imply it, so a token minted to let something author workflows cannot remove one.

Terminal window
curl -X DELETE https://api.dagweave.com/api/v1/workflows/wf-04c9 \
-H "Authorization: Bearer $DAGWEAVE_TOKEN" \
-H "Idempotency-Key: $(uuidgen)"

Success is a 204 with no body. A 404 covers the same three cases an update’s does. Retry under the same key and you get the 204 again with Idempotency-Replayed: true on it, rather than being told there is no such workflow.

There is no undo and no wastebasket. The row is gone, the id stops resolving, and every past run that pointed at that workflow is left pointing at nothing.

The reach goes past the row. A delete also clears the workflow’s template references and then sweeps up after them: any WorkflowTemplate dagweave installed on your cluster on that workflow’s behalf, and that nothing else now references, is deleted from the cluster as well. Deleting the last workflow that used a catalog template is what uninstalls the template.

The sweep only ever reaches names dagweave installed itself and recorded, and it is deliberately conservative about those:

  • A template any other workflow of yours still references is kept.
  • A template on a cluster and namespace with a run still going is kept, because a submitted workflow needs its backing template until the run ends.
  • A cluster-scoped install is never removed. It is readable from every namespace on the cluster, so your own workflows not referencing it is no evidence that nobody is.
  • A building block you vetted yourself is never removed.

The sweep runs after the workflow is already gone and is best effort, so a failure leaves an unreferenced template behind rather than failing the delete. It never goes the other way: nothing still in use is removed to make a delete succeed.

That reach is the argument for workflows:delete being separate rather than folded into the write scope. A token holding workflows:write can already save an empty graph over anything your team owns, which destroys the contents. What it cannot do is take the row away, leave every past run pointing at nothing, and uninstall a template from your cluster.

Requires audit:read. Returns your team’s audit events, newest first. A plan without the audit log is a 402, exactly as it is in the UI.

  • action matches an action name.
  • actor takes a user id or an email address.
  • outcome matches the recorded outcome.
  • from and to bound when the event happened, as RFC3339 timestamps.
  • before is an RFC3339 cursor for paging backwards.
  • limit caps how many events come back.
  • format=csv returns a CSV attachment instead of JSON.

A timestamp that is not RFC3339 is a 400. Each JSON event carries id, occurredAt, action, and outcome, plus actorEmail, actorIp, objectType, objectId, and details where they apply. The internal actor id never leaves the server, so events are keyed by the actor’s email. The CSV export carries details as a column of raw JSON, so a reviewer handed the export is not told less than the API tells you: it is where a run dispatch records the connector it went to, and where a refusal records its reason. id is the one field the CSV leaves out.

Budgets run in fixed one-minute windows and are counted twice: a ceiling for the whole team, and a smaller cap per user underneath it. One member cannot drain the team’s budget.

A response reports whichever budget binds you first. X-RateLimit-Limit is that budget, X-RateLimit-Remaining is what is left of it, and X-RateLimit-Reset is the Unix timestamp when the window rolls over. Go past it and you get a 429 with Retry-After in seconds.

The headers only appear once the token has been resolved. Two refusals happen before that and carry none of them: a 401 for a missing, unknown, expired or revoked token, and a 403 for a token pinned to a team you have left. A missing header is not a budget of zero.

A 403 for a missing scope does carry the headers, because the budget was already charged.

Every request made with a live token is charged, including one that is then refused for a missing scope.

Every error body is a JSON object with a single error key.

  • 400 means the request was malformed: a body field the endpoint does not declare, a timestamp that is not RFC3339, a run request missing connectorId or namespace, an update that does not quote a baseVersion, a relabelling that does not name labels, a rerun of a test run, or any write sent without an Idempotency-Key.
  • 401 means the Authorization header was missing, or the token is unknown, revoked, or past its expiry. Those last three are answered identically on purpose, so guessing at tokens tells you nothing; a missing header is the one that reads differently.
  • 402 means your plan does not include the feature behind the endpoint: the audit read, or a save carrying a node type or a catalog template your plan does not cover.
  • 403 means the token does not carry the scope the endpoint requires, or it is pinned to a team you no longer belong to, or the person who minted it may not do what you asked. A viewer’s token carrying runs:write lands here.
  • 404 on a single run means it does not exist or is not yours. On a run request it means the workflow does not exist, or the connector does not. On a write against a stored workflow it means the id does not exist, belongs to another team, or is one of the seeded examples. On a node type it means no such type, and the message names the ones there are.
  • 409 comes from a write’s Idempotency-Key, when the key was already spent on a different request or the first call using it has not answered yet, and from an update quoting a baseVersion that is no longer the stored one.
  • 422 comes only from a save. The body is the draft check’s own answer, so the faults tell you what to fix. Nothing was stored.
  • 429 means you are over the rate limit for the current window. A run request and a create can also answer 429 for a reason that is not the request budget, the plan’s workflow count in the create’s case, and that one carries no Retry-After.
  • 503 on a run request means the target connector is enrolled but has not been heard from recently. Nothing was dispatched, so once it is back the same request is safe to retry under the same key.

Every endpoint on this page is also served as a Model Context Protocol tool at POST /api/v1/mcp, so an LLM agent can pull run status, read a failing step’s logs and check a draft while you work out why a run failed. It takes the same API tokens and honours the same scopes, which means an agent sees exactly what the token’s owner sees and nothing more, and it can start a run only if you minted its token with runs:write. See MCP server for client setup and the tool reference.