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.
The same handler the UI calls
Section titled “The same handler the UI calls”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.
Authenticating
Section titled “Authenticating”Send the token as a bearer credential:
curl -H "Authorization: Bearer $DAGWEAVE_TOKEN" \ https://api.dagweave.com/api/v1/runsAn 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.
Scopes
Section titled “Scopes”A token carries one or more scopes. Each endpoint requires exactly one.
runs:readfor the run list and the single-run read.runs:logsfor one step’s log output.runs:writefor starting a run and for rerunning one.workflows:readfor the workflow list, the node types, and the draft check.workflows:writefor saving a workflow, editing one, and setting its labels.workflows:deletefor removing one.audit:readfor 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.
GET /api/v1/runs
Section titled “GET /api/v1/runs”Requires runs:read. Returns your team’s runs, newest first, as a JSON array.
limitsets the page size. It defaults to 50 and is capped at 200.offsetskips that many rows.includeTest=trueincludes 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.
GET /api/v1/runs/{id}
Section titled “GET /api/v1/runs/{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 withid,name,phase,message, and its ownstartedAtandfinishedAt. A step that declared them also carries aresourcesblock holdingrequestsandlimits, each withcpuandmemoryas the Kubernetes quantity strings you wrote, such as100mand128Mi.containerStartedAtis when the container itself began, which is later thanstartedAtby however long the step queued. A step that waited on something carrieswaitwith thekindit waited for, and a step on a traced cluster carriessegments, the named parts of its time in milliseconds.artifacts, each withname,bucket,key, andurl.logs, each withnodeIdandlocation. The location points at your own storage, so the log bytes do not pass through dagweave.
GET /api/v1/runs/{id}/nodes/{nodeId}/logs
Section titled “GET /api/v1/runs/{id}/nodes/{nodeId}/logs”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.
tailcaps the trailing lines returned. It defaults to 200 and is capped at 5000.sinceis a Go duration such as30mor2h, narrowing the range read. It is capped at720h, and anything unparseable is ignored rather than refused.containers=alladds 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:
pointerputs the log object’s address inlocation. dagweave does not hold your object-store credentials, so it hands you the pointer and you fetch the bytes yourself.contentputs the lines themselves incontent, served from storage dagweave does hold.collectingmeans lines are expected for this step but have not landed yet. Ask again shortly.expiredandnonemean there is nothing to return, andmessagesays which of the two it is.
A log backend that fails is a 502, because that is not your request’s fault.
GET /api/v1/workflows
Section titled “GET /api/v1/workflows”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.
qmatches a substring of the name, case-insensitively.label,namespace, andfolderare exact-match facets.limitandoffsetpage 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.
GET /api/v1/node-types
Section titled “GET /api/v1/node-types”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.
typeIdnarrows 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.
POST /api/v1/workflows/validate
Section titled “POST /api/v1/workflows/validate”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.
POST /api/v1/workflows/{id}/run
Section titled “POST /api/v1/workflows/{id}/run”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.
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"}'connectorIdis required and names the connector to dispatch to.namespaceis required and names the Kubernetes namespace on that connector’s cluster.argoInstanceIdpicks the instance when the connector serves more than one.paramsis 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.
The Idempotency-Key header
Section titled “The Idempotency-Key header”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: trueon 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.
POST /api/v1/runs/{id}/rerun
Section titled “POST /api/v1/runs/{id}/rerun”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.
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.
POST /api/v1/workflows
Section titled “POST /api/v1/workflows”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.
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 acodefield readingemail_unverifiednext to the message. - Over your plan’s workflow count is a
429. It carries noRetry-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.
PUT /api/v1/workflows/{id}
Section titled “PUT /api/v1/workflows/{id}”Requires workflows:write. Replaces a stored workflow with a new whole document
and returns its new version.
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.
PUT /api/v1/workflows/{id}/labels
Section titled “PUT /api/v1/workflows/{id}/labels”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.
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.
DELETE /api/v1/workflows/{id}
Section titled “DELETE /api/v1/workflows/{id}”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.
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.
What does not come back
Section titled “What does not come back”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.
GET /api/v1/audit
Section titled “GET /api/v1/audit”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.
actionmatches an action name.actortakes a user id or an email address.outcomematches the recorded outcome.fromandtobound when the event happened, as RFC3339 timestamps.beforeis an RFC3339 cursor for paging backwards.limitcaps how many events come back.format=csvreturns 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.
Rate limits
Section titled “Rate limits”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.
What the failures mean
Section titled “What the failures mean”Every error body is a JSON object with a single error key.
400means the request was malformed: a body field the endpoint does not declare, a timestamp that is not RFC3339, a run request missingconnectorIdornamespace, an update that does not quote abaseVersion, a relabelling that does not namelabels, a rerun of a test run, or any write sent without anIdempotency-Key.401means theAuthorizationheader 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.402means 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.403means 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 carryingruns:writelands here.404on 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.409comes from a write’sIdempotency-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 abaseVersionthat is no longer the stored one.422comes only from a save. The body is the draft check’s own answer, so the faults tell you what to fix. Nothing was stored.429means you are over the rate limit for the current window. A run request and a create can also answer429for a reason that is not the request budget, the plan’s workflow count in the create’s case, and that one carries noRetry-After.503on 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.
MCP access
Section titled “MCP access”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.