.md

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

  1. 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_url
    Teams on failure Posts a Microsoft Teams Adaptive Card for the event, with an Open project button teams_webhook_url
    Webhook on failure Forwards the alert payload as JSON to any HTTPS endpoint, with the event in the x-dcupl-event header report_webhook_url

    Despite 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.

  2. 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.
  3. 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.

  4. Create a rule under Project Settings → Reporting: the event, the workflows to watch, this workflow as the target, and a cooldown. See Rules.

  5. 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:

  1. A cron trigger on your schedule.
  2. A dcupl API node, operation Reporting digest, with period (Daily or Weekly) and timezone (an IANA zone, e.g. Europe/Vienna). It fetches the most recently closed period: yesterday for daily, the seven days ending yesterday for weekly.
  3. A Script node that turns the digest payload into a message.
  4. 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:

  1. A Settings block of plain constants, the things most people change:

    Setting Templates What it does
    HEADER / FOOTER both digests First and last paragraph (mail) or line (Slack) of the message. Plain text; set to '' to leave it out.
    CONSOLE_URL all 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_PAGE all four The page the link opens inside the project. Default: the workflows overview, /draft/workflows/v3/templates/overview.
    LINK_LABEL both digests The text of the project link.
    SHOW_PROJECT_LINK Slack / Teams on failure false removes the link (Slack) or the Open project button (Teams).
    MAX_ERRORS both digests How many of the most frequent errors to list (the payload carries at most 10).
  2. Numbered steps: read the payload, name the values the message is built from, build the message. Change the wording where the strings are.

  3. One return with 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 their esc() 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, text and html, 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 through esc() 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
}
  • event is run-failed, run-completed or workflow-recovered.
  • workflow.name is the workflow's readable name (the key when it has none); use it in messages. workflow.key is the deployed key with the project ID.
  • entry.error (message, nodeId) is present only for a failed run.
  • suppressedCount is the number of runs the cooldown held back since the last call for this rule and workflow; always 0 for workflow-recovered.
  • triggerType is how the run was started: debug, cron or request. apiPath is 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.key is YYYY-MM-DD for a daily digest and an ISO week YYYY-Www for a weekly one; period.label is the readable form the messages show.
  • totals.successRate is 0–1 (0 when runs is 0); avgDuration is in rounded milliseconds.
  • workflows has one row per workflow that ran in the period, sorted by failed descending, then runs descending, then workflowKey.
  • topErrors holds up to 10 distinct failure messages, sorted by count descending, each with the keys (workflowKeys) and names (workflowNames) of the workflows it occurred on.
  • generatedAt is 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 with 400 "subject must not contain line breaks").
  • text and/or html — 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 409 on its Error port.
  • Runners in your own infrastructure send the same run metadata to dcupl (never item data). Set DCUPL_RUN_REPORTING=false on such a runner to opt it out; no runs are reported and its boot log says "Run reporting disabled".