Reporting
Reporting tells you how your workflows are doing, in two ways:
- Alerts. A rule watches your deployed workflows for an event — a run failed, a run completed, or a workflow recovered — and when it fires, it calls a reporting workflow with the details. That workflow posts to Slack, to Teams or to any webhook.
- Digests. A scheduled workflow fetches a daily or weekly summary of your runs and mails or posts it. No rule is involved.
Both are ordinary workflows made from starter templates, so you can open and change them like any other workflow. It is the same model n8n calls an "error workflow".
flowchart LR W[Run of a watched workflow] -->|failed · completed · recovered| Rule[Reporting rule] Rule -->|POST with the run details| T[Reporting workflow] T --> Slack T --> Teams T --> Webhook
flowchart LR Cron[Cron trigger] --> Digest[dcupl API: digest] Digest --> Script[Format digest] Script --> Mail[dcupl API: email] Script --> Slack
Runners report every workflow run to dcupl, but only its metadata: execution id, status, timing, trigger info and, for a failed run, the error message and node id. Item data is never sent.
Before you start
- You need to be an owner or editor of the project: only they see the Reporting tab under Project Settings.
- The project needs two features, both on by default for new projects:
Workflows and Workflow reporting. If either is off, the Reporting tab is
hidden and the reporting API answers
400; ask your dcupl contact to switch them on. A project can have up to 20 rules. - Runners hosted by dcupl report their runs without any setup. For a runner in your own infrastructure, see Limits and notes.
Send alerts to Slack, Teams or a webhook
Create the reporting workflow. Project Settings → Reporting → New reporting workflow opens the Create Workflow dialog on the Workflows page with the Reporting shelf of starter templates. (Creating a workflow on the Workflows page opens the same dialog.) Pick one:
Template What it does Variable Slack on failure Posts a short message to a Slack incoming webhook: the event, the workflow name in bold, and a link to the project slack_webhook_urlTeams on failure Posts a Microsoft Teams Adaptive Card for the event, with an Open project button teams_webhook_urlWebhook on failure Forwards the alert payload as JSON to any HTTPS endpoint, with the event in the x-dcupl-eventheaderreport_webhook_urlDespite their names, all three handle all three events (
❌ … failed,✔ … completed in N ms,✅ … recovered): pair any of them with a Run failed, Run completed or Workflow recovered rule.Set the variable. The template declares its one variable with an empty value, so the deploy dialog lists it as a gap you fill in per deployment. Or define a project variable with the same name once and reuse it across workflows. None of the templates needs an API key.
- Slack: create (or reuse) a Slack app with an Incoming Webhook for
the channel and copy its
https://hooks.slack.com/services/…URL. - Teams: add the Incoming Webhook connector to the channel, or create a webhook with the Teams Workflows app, and copy its URL.
- Webhook: any HTTPS endpoint you control (PagerDuty, Discord via a
relay, your own service). It has to be public: a workflow's Request node
cannot reach private or loopback addresses such as
localhost.
- Slack: create (or reuse) a Slack app with an Incoming Webhook for
the channel and copy its
Deploy it to a runner like any other workflow. Its request trigger is open: anyone who knows the runner URL and the workflow key can post into your channel. If the runner is reachable from the internet, add an API key to the trigger; the rule sends that key when it calls the workflow.
Create a rule under Project Settings → Reporting: the event, the workflows to watch, this workflow as the target, and a cooldown. See Rules.
Test it. Execute in the editor runs the workflow without a reporting payload: the Slack and Teams templates post "No reporting payload — trigger this workflow from a reporting rule.", and the Webhook template forwards the body you sent. To preview a real message, paste a payload like the one in The alert payload into the test's Request Body (JSON), or let a watched workflow fail, complete or recover.
Every template carries a canvas note (the yellow sticky note near the trigger): how it works, the variable to set, and how to test it.
Send a daily or weekly digest
The two digest templates schedule themselves (cron 0 0 8 * * *, 08:00 UTC),
so you create one, set its variable and deploy it — no rule needed:
| Template | What it does | Variable |
|---|---|---|
| Daily digest to mail | Fetches yesterday's digest (UTC) with the dcupl API node, formats it as an HTML mail with a plain-text fallback, and sends it with the dcupl API node | reporting_email_to: up to 10 addresses, comma-separated |
| Daily digest to Slack | Fetches the digest the same way and posts it to a Slack incoming webhook | slack_webhook_url |
For a weekly digest, open the deployed workflow, change the cron trigger to the weekday you want, and change the digest node's Period from Daily to Weekly; there is no separate weekly template. Execute in the editor makes a real call, so a test sends the actual digest.
To build a digest from scratch:
- A cron trigger on your schedule.
- A dcupl API node, operation Reporting digest, with
period(Daily or Weekly) andtimezone(an IANA zone, e.g.Europe/Vienna). It fetches the most recently closed period: yesterday for daily, the seven days ending yesterday for weekly. - A Script node that turns the digest payload into a message.
- A dcupl API node (Send e-mail) or a Request node (Slack, Teams, a webhook) that delivers it.
Rules
Project Settings → Reporting lists the project's rules, where you create, edit, switch on and off, and delete them. Rules react to single runs only; there is no digest event and no schedule on a rule. A rule has:
- Name — a label for the list.
- Event — Run failed, Run completed or Workflow recovered.
- Workflows — All workflows, or a selection of deployed workflows to watch.
- Target — the reporting workflow to call. It needs a POST request trigger.
- Cooldown (minutes) — 0 to 1440 (24 hours), for Run failed and Run completed, see below.
- Enabled — switch the rule off without deleting it.
What the events fire on:
- Run failed — a failed run of a watched workflow.
- Run completed — every completed run. With All workflows that is every completed run of every deployed workflow, and the form warns: "This fires for every completed run of every workflow. Select specific workflows to keep the target quiet."
- Workflow recovered — once per outage: the first completed run directly after a failed run of the same workflow. A completed run after a completed run, or a workflow's very first run, does not fire it.
A cancelled run fires nothing, and a cancelled run between a failure and the next completed run means that no recovery is reported: only a direct failed → completed change counts.
Cooldown. Once a rule fires for a workflow, further matching runs of that
workflow are counted but not reported until the cooldown has passed; the next
call reports them in suppressedCount ((+N more) in the messages). A new rule
starts at 15 minutes, and at 0 once you pick Run completed; 0 reports every
run. Workflow recovered has no cooldown setting, since it fires only once per
outage anyway.
A target that watches itself. When the target is one of the watched
workflows, its own runs are not reported, while the other watched workflows'
runs still are. The form shows a note but saves the rule. Two rules whose
targets watch each other can call each other in a loop if both have a cooldown
of 0: keep a cooldown of at least 1 minute on such rules, and don't point a
Workflow recovered rule at a target it also watches.
Customize the message
The message is built by one script step per template (Format message, Build Adaptive Card, Format digest). Open it and edit it; it is written to be changed. Every script reads top to bottom the same way:
A Settings block of plain constants, the things most people change:
Setting Templates What it does HEADER/FOOTERboth digests First and last paragraph (mail) or line (Slack) of the message. Plain text; set to ''to leave it out.CONSOLE_URLall four The Console the link points to. Pre-filled with the Console you created the workflow in; the link is CONSOLE_URL + '/app/p/' + projectId + LINK_PAGE.LINK_PAGEall four The page the link opens inside the project. Default: the workflows overview, /draft/workflows/v3/templates/overview.LINK_LABELboth digests The text of the project link. SHOW_PROJECT_LINKSlack / Teams on failure falseremoves the link (Slack) or the Open project button (Teams).MAX_ERRORSboth digests How many of the most frequent errors to list (the payload carries at most 10). Numbered steps: read the payload, name the values the message is built from, build the message. Change the wording where the strings are.
One
returnwith the fields the next step sends.
The alert templates receive the payload as the request body: a script reads it
as $json.first().body. Workflows are called by their name
(workflow.name / workflowName), falling back to the key, since deployed
keys carry the project ID and are hard to read.
Formatting depends on where the message goes:
- Slack uses mrkdwn:
*bold*,_italic_,<https://…|link text>. The scripts escape&,<and>in text that comes from a run (names, error messages) with theiresc()helper. Keep using it when you add such text, or an error message containing<!channel>becomes a real mention. - Teams text blocks take a Markdown subset:
**bold**,_italic_,[link text](https://…). - E-mail is sent as HTML with a plain-text fallback; the script returns
subject,textandhtml, so edit both bodies if you change the content. The HTML is deliberately simple (inline styles, one table), because mail clients ignore stylesheets and scripts. Foreign text goes throughesc()so an error message can't inject markup.
The Slack, Teams and webhook request steps send the script's item as their
whole JSON body ({{$json}}), so a script never has to escape anything for
JSON: add a field to the returned object and it is sent (e.g. Slack's blocks
for Block Kit layouts).
The dcupl API node
The dcupl API node (palette section Nodes, "Reporting mails, digests, runs") lets a workflow read or send reporting data itself. Digests are made of it, and it also works on its own, e.g. in a workflow that mails on demand or lists recent runs.
The node authenticates as the runner: you don't create, store or pass an API key. Every other integration node (Request, dcupl Files, …) carries its own credentials. The project is the one of the deployed workflow the node runs in.
Pick an operation, which decides the rest of the fields:
| Operation | Fields | Output (one item unless noted) |
|---|---|---|
| Send e-mail | to (comma-separated addresses or a JSON array; templated), subject (templated), text and/or html (templated, at least one required) |
{ accepted, to } — one item per input item, to normalized to an array |
| Reporting digest | period (Daily / Weekly), timezone (IANA, e.g. Europe/Vienna) |
The digest payload for the closed period |
| Workflow runs | status (optional multi-select: Completed / Failed / Cancelled — omit for all), sinceHours (look-back window, default 24, max 720), limit (default 100, max 500) |
One item per run (none if the window is empty) |
The Send e-mail fields are templated like the Request node's
({{$json.field}}, {{variables.name}}) and filled in per input item, so the
node sends one mail per item. Reporting digest and Workflow runs make
one call, however many items arrive.
The node has three output ports:
- Success — the call went through.
- Permission Denied — dcupl answered 401/403: it rejected the runner's own credentials (for example, because the runner's key changed).
- Error — anything else: a validation error, reporting switched off for the
project (
400), a workflow dcupl can't find on this runner (404) or can't attribute to one project (409, see Limits and notes), a server error, a network or timeout failure, or a runner that isn't connected to dcupl at all. The item carries{ error: { message, statusCode? } }.
Wire Permission Denied and Error to their own branch (a Slack alert, a script that logs and stops, …) as you would for any other integration node, so a bad runner key or a switched-off feature doesn't fail silently.
The alert payload
When a rule fires, dcupl POSTs a ReportingInvocationPayload as JSON to the
target workflow's request trigger, as an asynchronous run:
{
"event": "run-failed",
"projectId": "6DssgYWUvwzpFiWiZRkw",
"rule": { "uid": "rule_abc123", "name": "Slack on failure" },
"workflow": { "uid": "wf_def456", "key": "6DssgYWUvwzpFiWiZRkw-always-fails", "name": "Always fails" },
"entry": {
"executionId": "exec_789",
"workflowKey": "6DssgYWUvwzpFiWiZRkw-always-fails",
"status": "failed",
"startedAt": 1757404800000,
"duration": 842,
"metadata": {
"nodesExecuted": 3,
"itemsProcessed": 12,
"completedNodes": 2,
"failedNodes": 1
},
"error": { "message": "boom", "nodeId": "transform" },
"triggerType": "request",
"apiPath": "/webhook",
"runnerUid": "zaKeUgvEk6jDyk7Jl0je",
"workflowUid": "wf_def456"
},
"suppressedCount": 2,
"firedAt": 1757404860000
}eventisrun-failed,run-completedorworkflow-recovered.workflow.nameis the workflow's readable name (the key when it has none); use it in messages.workflow.keyis the deployed key with the project ID.entry.error(message,nodeId) is present only for a failed run.suppressedCountis the number of runs the cooldown held back since the last call for this rule and workflow; always0forworkflow-recovered.triggerTypeis how the run was started:debug,cronorrequest.apiPathis set for request-triggered runs.
The digest payload
The Reporting digest operation returns one item:
{
"projectId": "6DssgYWUvwzpFiWiZRkw",
"period": {
"key": "2026-09-08",
"start": 1788818400000,
"end": 1788904800000,
"timezone": "Europe/Vienna",
"label": "Tue 2026-09-08"
},
"totals": { "runs": 40, "completed": 37, "failed": 3, "cancelled": 0, "successRate": 0.925, "avgDuration": 842 },
"workflows": [
{
"workflowUid": "wf_def456",
"workflowKey": "…-always-fails",
"workflowName": "Always fails",
"runs": 5,
"completed": 2,
"failed": 3,
"cancelled": 0,
"avgDuration": 601
}
],
"topErrors": [
{ "message": "boom", "count": 3, "workflowKeys": ["…-always-fails"], "workflowNames": ["Always fails"] }
],
"generatedAt": 1788933900000
}period.keyisYYYY-MM-DDfor a daily digest and an ISO weekYYYY-Wwwfor a weekly one;period.labelis the readable form the messages show.totals.successRateis0–1(0whenrunsis0);avgDurationis in rounded milliseconds.workflowshas one row per workflow that ran in the period, sorted byfaileddescending, thenrunsdescending, thenworkflowKey.topErrorsholds up to 10 distinct failure messages, sorted bycountdescending, each with the keys (workflowKeys) and names (workflowNames) of the workflows it occurred on.generatedAtis when the digest was built, not when the period ended.
Reporting API endpoints
Two endpoints serve callers outside a workflow, such as scripts or CI jobs, that
hold a project CLI API key (Project Settings → API Keys → type "CLI"). A
reporting workflow never needs them: its dcupl API node reaches the same
operations without a key. Both live on https://api.dcupl.com; send the key in
an api-key header.
POST /projects/:id/reporting/email sends a transactional e-mail:
{
"to": ["ops@example.com", "eng@example.com"],
"subject": "dcupl digest: 42 runs, 3 failed",
"text": "…"
}to— an array of 1–10 addresses (after trimming and removing duplicates), or one comma-separated string.subject— trimmed, up to 200 characters, no line breaks (rejected with400 "subject must not contain line breaks").textand/orhtml— at least one with non-whitespace content, each up to 100 kB.
It answers 200 { "accepted": <n> }, 400 on a validation error, and
502 "Mail delivery failed" when the mail can't be delivered. Mails come from
no-reply@dcupl.com, display name "dcupl Reporting". It needs the owner or
editor role.
GET /projects/:id/reporting/executions lists recorded runs. Query
parameters: workflowUid, status (comma-separated), since (epoch ms) and
limit (1–500, default 100). dcupl keeps up to 100 runs per workflow for 30
days. It needs the viewer role or above.
Limits and notes
- Three events only:
run-failed,run-completed,workflow-recovered. Digests are scheduled workflows, not rule events. - A recovery right after a restart of the dcupl platform may go unreported, because Workflow recovered compares a run with the workflow's previous one.
- Digest totals are counted per UTC day. For a timezone away from UTC, the window a digest covers can shift by part of a day at its edges, most visibly west of UTC around local midnight.
- The dcupl API node works only on a runner connected to dcupl. On a local or offline runner it fails on its Error port with a message saying so. That is different from Permission Denied, where the runner is connected but the call was rejected.
- A workflow key two projects deploy on the same runner is ambiguous. dcupl
finds the calling project from the workflow key alone, so that one project's
workflow can never read another project's data on a shared runner. If two
projects deploy a workflow under the same key on the same runner, the dcupl
API node gets
409on its Error port. - Runners in your own infrastructure send the same run metadata to dcupl
(never item data). Set
DCUPL_RUN_REPORTING=falseon such a runner to opt it out; no runs are reported and its boot log says "Run reporting disabled".