Flows: visual automation that runs¶
Build a graph — start, actions, transforms, conditions, sub-flows, end — connect it, and Datahub runs it on a schedule, on a webhook, on a platform event, or on demand. Think n8n inside Datahub, with first-class versioning, replay, audit, and AI assistance.
Flows is where Processes-the-documentation meets execution: turn a written procedure into something the platform actually does.
When to choose this¶
-
Run an HTTP call when something happens
Trigger by webhook, schedule, or platform event, then POST to your CRM, ERP, or Slack.
-
Wire a multi-step automation
Conditional branching, parallel execution, sub-flows, and retries in one graph.
-
React to a platform event
A contract is published → enrich it downstream. An alert fires → page on-call.
-
Compose flows from flows
Build small, reusable atomic flows and invoke them from a parent flow.
When not to reach for Flows
How a flow runs¶
A trigger hands a payload to the engine, which walks the active version's graph node by node. Each step reads the execution context, does its work, and writes the context forward to the next step. Everything is recorded for audit and replay.
flowchart LR
M["Manual run"] --> ENG
S["Schedule<br/>(cron)"] --> ENG
W["Webhook<br/>(HMAC-signed)"] --> ENG
E["Platform event"] --> ENG
ENG(["Flow engine"]) --> VER["Active version<br/>nodes + edges"]
VER --> STEP["Step handlers<br/>Action · Transform · Condition · Sub-flow"]
STEP <--> CTX[("Execution context<br/>JSONB · redacted · 64 KiB")]
STEP --> DONE(["End"])
STEP -.records.-> AUDIT[("Audit + replay<br/>SCD2 version history")]
classDef accent fill:#ff8400,stroke:#e67700,color:#ffffff,font-weight:600;
class ENG accent
Figure 1 — Any of four triggers starts an instance; the engine runs the active version's graph, threading a redacted context through each step and recording everything for replay.
Why teams reach for this
Small reusable flows compose into big automations, every run is replayable from any step with the exact (redacted) context it originally saw, and every change to a flow is audited with field-level diffs. You get n8n-style automation without giving up governance.
Concepts¶
| Concept | What it is |
|---|---|
| Flow definition | The reusable template. Name, trigger, version chain, schedule(s), event subscription(s), webhook secret, variables. |
| Flow version | A snapshot of the graph (nodes, edges, node configs). Versions auto-increment; only one is active at a time. |
| Flow instance | One execution of an active version. Carries a context (JSONB) through every step. |
| Step execution | One step's run within an instance. Records input context, output context, duration, status, error. |
| Step handler | The code that runs for a node type — built-in handlers for transform, condition, switch, loop, delay, capability and outbound webhook. |
| Trigger | What starts a flow: manual, schedule (cron), webhook, platform event, or sub-flow invocation. |
| Variables | Flow-scoped key-value pairs injected into the execution context at start. |
| Template | A publishable, portable flow definition with category and tags; importable into another tenant. |
Node types¶
| Node | Category | What it does |
|---|---|---|
| Start | control | Entry point. |
| End | control | Termination. |
| Transform | step | Copy a value between context keys, or compute one with an expression — see Transform expressions below. |
| Condition | control | Compare one context field against a value (field · operator · value) → branch on true / false via the edge's source handle. It takes a field name, not an expression — see Transform expressions for where expressions do apply. |
| Switch | control | Route on a field's value: each outgoing edge carries the value it matches, with a default catch-all. |
| Loop | step | Iterate a list in the context. The loop handle feeds the body, which edges back into the node; the done handle continues once the list is exhausted. Capped at 1000 iterations (100 by default). |
| Delay | control | Pause until a wall-clock deadline, then continue automatically. |
| Webhook | step | Send an outbound HTTP request through an approved connection — see Inbound trigger vs outbound step below. |
| Sub-flow | step | Invoke another flow as a child step (max depth 5; cascade prevention). |
| Sync (join) | control | Join parallel branches before continuing. |
| Capability | step | Invoke a governed platform operation (Run Metric, Warehouse Query, Notify, Catalog Query, or Call MCP Tool) — see Flow Capabilities. |
| Code | step | Runs a Python function on values from the flow, in a sandbox with no network access, and stores the dict it returns. Each run uses one code run from your licence — see Python code steps. |
| AI Agent | step | Runs a prompt through HERC. Tool access is scoped to the flow's accountable owner — the step can only use the tools that owner's assigned roles already permit; an owner with no eligible role, or a flow with no owner, still runs the step but with no tool access at all. |
Condition operators: ==, !=, >, <, >=, <=, in, not_in, is_empty, is_not_empty.
Inbound trigger vs outbound step¶
"Webhook" means two unrelated things in a flow, and they never overlap:
- An inbound webhook is a trigger — how a flow starts. You set it on the flow itself, and DataHub gives you a URL and a signing secret for the external system to call.
- The Webhook node is a step — how a flow calls out. It sends a request partway through a run.
The Webhook node has no URL field, and that is deliberate: it dispatches an approved outbound operation that an administrator has registered as a governed connection. The method, path, credentials and the hosts it may reach all come from that operation, so a flow can only call endpoints your organisation has already sanctioned. If the picker is empty, no outbound operations have been registered yet.
Datahub reads at most 10 MB of the answer. A larger answer fails the step with a message that names the host and the limit, and none of it is kept (see Limitations).
graph LR
EXT[External system] -->|HMAC-signed POST| TRG[Inbound webhook trigger]
TRG --> ENG[Flow engine]
ENG --> HOOK[Webhook step]
HOOK -->|approved operation| API[External API]
classDef accent fill:#FF6B4A,stroke:#FF6B4A,color:#fff
class HOOK accent
What Flows looks like¶
| Surface | Where | What you see |
|---|---|---|
| Landing | /flows |
KPI strip (active flows, success rate, runs today, avg duration), mini execution timeline, health signals card, AI suggestions, definitions table with create / search / filter / bulk select. |
| Templates browser | /flows/templates, opened with Start from a recipe when you create a flow (not in the Flows menu for now) |
Card grid with category filter, search, Use template, import / export. Five business recipes ship out of the box and are listed first. |
| Analytics | /flows/analytics |
Per-definition success / failure / duration metrics, stacked timeline, step-level analytics, dependency graph showing cross-flow sub-flow references. |
| Builder | /flows/{id} |
Floating-panel canvas, palette, config panel, version switcher, run history panel, schedule + event panels, audit log, variables panel, webhook management, Debug tab. |
| Run detail | /flows/{id}/runs/{instanceId} |
Step timeline (pulse on running steps, expandable input / output context), execution logs in your language (virtualised, level filtered), execution tree (parent / child sub-flows), retry from failure, replay-from-step. |
Setup — what an admin does once¶
| Prereq | Where | Why |
|---|---|---|
| Roles | /rolegroups |
flow.definitions.manage to author flows; flow.instances.read to view executions. flow.instances.read on its own opens Flows in the sidebar: the overview (run counts, timeline, health signals and each flow's latest run), Runs, run details and Analytics, without any authoring action. flow.definitions.read on its own opens Flows too, but only to see which flows exist: in the app it shows the list of flows and their counts, read-only, and opening a flow in the editor needs flow.definitions.manage. The same role also lets the API and AI connectors (MCP) read a flow's definition and versions. flow.definitions.manage and flow.definitions.author also include integrations.read, so a flow's metric, message and MCP steps list the organisation's connections, by name and type only, never their credentials. When the list can't be read or doesn't load, the step says so instead of saying that none are set up. |
| AI provider key | /admin/integrations → AI Provider Keys |
Powers the HERC button on the canvas and the AI agent that answers questions about flows. The agent can also build a flow from chat: like the builder, it asks which of your domains the flow belongs to, and before submitting it for review it lists the published data contracts and asks you to confirm the one the flow runs under. When the platform refuses a step, the agent repeats the platform's reason, for example the domain your role groups do not govern. |
| Webhook auth (per flow) | Builder → Webhook panel → Reveal secret | Each definition gets an auto-generated 128-char webhook secret. The sender signs the raw body with HMAC-SHA256 and sends the hex digest in X-DataHub-Signature. Rotate from the same panel. |
| Schedules / events | Builder → Schedule panel / Event subscription panel | If you want the flow to run automatically. |
Building a flow¶
- Create.
/flows→ New flow → name, description, trigger type, tags. Auto-creates version 1 with an empty graph. - Drag from the palette. Start → Actions / Transforms / Conditions / Sub-flows → End.
- Configure each node. Click a node → floating config panel with type-specific fields and inline validation.
- Pick fields, don't type keys. Some settings read a value that an earlier step wrote: the If / else condition, Route by value, For each, Set a value and a step's skip condition. You pick that value from a list grouped by the step that wrote it, such as Check revenue · value. After a test run, each field shows the value it had in that run. A message or prompt offers the same fields under Insert variable.
- Typed keys still work. For a key no step lists, choose Type a key instead. A flow saved with typed keys opens unchanged.
- Only what the step can read. The list holds only steps before this one on its own path. A step inside a For each gets the loop's item. The flow's own variables are listed too, under From the flow's variables, and so are the keys a sub-flow step maps back.
- A name, or a name in braces. A field that reads a value stores the name itself: pick or type
status, ormetric_result.value. Text puts the name in braces, so a message, an AI prompt or a human task's instructions hold{{status}}; Insert variable adds the braces for you. A Set a value expression reads a name with neither, and a nested value by index: its chips insertstatusormetric_result["value"]. - A warning for a name nothing sets. If a field names a key that no earlier step, flow variable or trigger sets, a warning shows under it once you stop typing: "No earlier step, flow variable or trigger sets stauts. Did you mean status?" Use status puts the listed key in. Braces typed into a field that reads the name itself get a warning too, with the name without braces. The step still saves: a value the run's own input brings (a manual run's JSON, an API call) is only listed after a test run with it.
- Last test run. Each step's panel has a Last test run block with what the step wrote and saw in the newest test run, or why it failed. It is redacted and capped like the run log.
- Set the trigger (see the tabs below).
- Variables. Variables panel → key-value pairs. Every run starts with them as top-level keys: a field reads
threshold, a message{{threshold}}. A value the run's own input brings for the same key wins. - Save. Auto-saves to the current draft version. Activate the version when ready.
- Run. Trigger-test from the builder, or wait for the schedule / webhook / event.
Click Run in the builder. Best for testing and for flows a human kicks off deliberately.
For a webhook or event flow, Run opens a short form with one box per field that the webhook's request contract or the event declares. Required fields are marked. Write JSON instead switches to the raw payload.
A run from the builder, Run from here on a step, the webhook test-send and the AI connector's test_flow_step are test runs of the version you have open. A test runs on a draft or on the active version. On a draft, the capabilities its own steps name are allowed for that test only (connection governance still decides every call); on the active version, the capabilities its approval froze. A version under review, or one a newer version replaced, has neither, so the builder turns these buttons off there and says why next to them, and a test started another way is refused: "Version 3 is under review, so it can't be test-run: a test runs on a draft or on the active version. It can be tested again once it is a draft: when its review is rejected, or withdrawn by the person who submitted it, the person or team it is assigned to, or a member of the Administrator role group." If you may withdraw the review yourself, the builder says so instead. Retrying a failed test, or a test that waited on a delay or a task, runs with the same capabilities as the test itself. If the draft was submitted for review in the meantime, the retry is refused with the same sentence and the waiting test stops with it.
Schedule panel → Add → choose How often: every few minutes, every few hours, daily, weekly or monthly, at a time in UTC, or Custom (cron) to type a cron. Nothing is preset: Create waits until you choose. The panel shows the cron it writes and the next runs in your own time zone. The Procrastinate periodic job picks due schedules and triggers them.
Webhook panel → reveal the URL + secret; POST anywhere with an X-DataHub-Signature: <hmac_sha256(raw_body, secret)> header. Rate limited to 60 requests / minute per definition.
Event subscription panel → subscribe to a platform event (e.g. contract.published, asset.created, alert_rule.triggered) with optional filters. The event bus delivers; cascade prevention guards against infinite loops.
Removing a schedule or event trigger¶
Archive on a schedule or event subscription archives it rather than deleting it. It stops starting the flow straight away, and it moves to the panel's Archived list, which shows when it was archived. Restore brings it back paused, so you choose when it starts the flow again by switching it on. An archived trigger can't be switched on or edited until it is restored.
The webhook trigger, verified¶
The webhook endpoint is public (no JWT) but every request is authenticated by an HMAC signature and throttled. The caller signs the raw body with the per-definition secret; Datahub recomputes the digest and compares in constant time.
sequenceDiagram
autonumber
participant C as Your system
participant DH as Datahub webhook
participant FE as Flow engine
C->>C: HMAC-SHA256(raw body, secret)
C->>DH: POST …/webhook<br/>X-DataHub-Signature: «hmac»
DH->>DH: Recompute HMAC · constant-time compare
alt valid signature & under rate limit
DH->>FE: Start instance (body = trigger payload)
FE-->>C: 202 Accepted
else bad / missing signature
DH-->>C: 403 Forbidden
else over 60 req/min
DH-->>C: 429 Too Many Requests
end
Figure 2 — Webhook authentication. A 403 means the signature didn't match; a 429 means you exceeded the per-definition rate limit.
Rotate the secret from the builder's Webhook panel any time you suspect leakage — the old secret is revoked immediately.
An administrator can also limit which addresses may call the webhook (the connection's source allowlist). A call from any other address gets a 403 as well. The check uses your system's own address as our edge sees it; a forwarding header your system sends cannot change it. If your organization puts its own CDN or proxy in front of Datahub, the address seen is that service's, so list its outgoing addresses.
Step input / output context¶
Every step records its input context and its output context as JSONB.
- The full context is redacted through
SecretRedactor(key-based recursive masking) before storage — keys like*token*,*secret*,*password*are masked. - The redacted context is capped at 64 KiB UTF-8 per field. Over the cap, the stored value is
{ "_truncated": true, "original_bytes": N }so you can see the size without storing the payload. - Output context becomes the next step's input context (with transform / mapping applied).
How replay stays safe
Because each step persists the exact redacted context it ran with, you can re-run from any step with that same context. Replay always starts a brand-new instance on the active version — the source run is never mutated, and the new run's superseded_by link makes the chain visible.
Version approval workflow¶
Every flow version must be approved before it becomes active, so a human reviewer consciously signs off on the capabilities a flow may use (querying metrics, sending notifications, …). The one exception is a flow built only from your organisation's approved steps, once an administrator has switched self-service publishing on: see Publishing without a review below.
stateDiagram-v2
direction LR
[*] --> Draft
Draft --> UnderReview: Submit for Review
UnderReview --> Draft: Reject / Withdraw
UnderReview --> Active: Approve
Active --> Draft: New version (edit)
Active --> Superseded: Newer version activated
Superseded --> [*]
Figure 3 — A version's lifecycle. Submitting freezes the graph; approval activates it and supersedes the previous active version. Datahub records which version was active at every point in time (SCD Type 2).
- Build — edit the graph in draft. Add capability nodes for the actions the flow needs.
- Submit for Review — validates the graph and starts an approval workflow; the version is frozen while under review.
- Review — a user with
flow.definitions.managesees a task in their Tasks inbox and approves or rejects. - Approve — the version becomes active with a frozen capability set; the previously active version is superseded.
- Reject / Withdraw — the version returns to draft for further editing. You can withdraw while the review is open. The people who may are whoever submitted it, the team or person it is waiting on, and an administrator; see Workflows.
Who reviews it. The submit window names the reviewer before you send it: a steward of the flow's domain, or a Data Owner, who holds flow.definitions.manage. When nobody there may approve it, the window asks you to choose a group, never a single person, and lists only groups with someone other than you who holds flow.definitions.manage. The review goes to that group's team inbox. When no group has such a member, Submit stays off and the window names the missing role: ask an administrator to give it to someone in a group, or to switch off the review requirement. Playbook versions work the same way from the playbook's page, except that there is no Data Owner fallback. A review never comes back to you: if nobody else turns out to be able to review it, it goes to the administrators' team inbox.
One playbook version in review at a time. While one version of a playbook is under review, Submit for review on a newer draft stays off, and the reason is shown under it. Withdraw the review from the playbook's page (Withdraw review of v1, for example) or wait for it to be approved or rejected, then submit the draft. The person who submitted the review, whoever it is assigned to, and an administrator with tasks.admin can withdraw it; anyone else is told to wait. Withdrawing returns that version to draft and closes its review task. Before the September 2026 update a playbook could have several versions in review at once, and approving an older one after a newer one went live put the older one back live. The update kept the newest review of each playbook and marked the older ones as superseded, with their reviews cancelled; each has a review_withdrawn entry in the audit log that says why.
Who can change a flow. A flow belongs to a domain, and only people whose role group works in that domain can change its name, description, variables, trigger and domain, or its steps: creating a new version, saving the canvas, deleting a draft, submitting a version for review or withdrawing it, and activating it directly when review is switched off. Starting a review from anywhere else in Datahub follows the same rule. So do archiving, restoring and permanently deleting the flow (one at a time or in bulk), rotating its webhook secret, and adding, changing or removing its schedules and event triggers. Datahub records who did each of these. Anyone else with the flows role can still open the flow, and is told "You are not a member of the Finance domain, so you cannot change this" when they try. Testing a draft counts as changing it, because a test runs steps no reviewer has approved yet: running a draft from the builder (Run, Run from here), sending it a test webhook and retrying a failed test of it all need the domain too, even for someone who manages flows. Running the live flow by hand does not. A flow with no domain stays editable by everyone with the flows role.
Every step must be able to run. When you submit, and again when the version is approved (or activated directly, with review switched off), Datahub checks that every Capability and Webhook step names something it can run: a built-in capability, or an active governed connection. A connection that is switched off after you submitted blocks the approval. The reviewer sees which step and which capability, and can approve again once the connection is back, or send the version back to you.
Publishing without a review. Someone who may approve flows (flow.versions.approve, which flow reviewers, administrators and the Platform.Approver tier hold) can let authors put a flow live themselves when every step in it is on the organisation's approved list. Authors can see the list but never change it, and an author cannot publish a flow in a domain their role group does not work in (a flow with no domain stays open, as for any other change). By default the list holds the control steps (start, end, condition, switch, loop, delay, transform), human tasks, and Capability steps that send a notification, run a metric, query the catalog or read a saved warehouse query. Any other step, such as an AI agent, a Call API or Webhook step, an MCP or code step, or a warehouse query with inline SQL instead of a saved query, still sends the version to review. A Capability or Webhook step counts by the capability it calls, never by its step type. Self-service publishing is off until someone who may approve flows switches it on and edits the list (GET and PUT /flowsmodule/self-service-policy).
Publishing (POST /flowsmodule/versions/{id}/publish) lets Datahub pick the route: live at once, to review, or blocked by a check that fails, such as a missing owner. GET /flowsmodule/versions/{id}/publish-readiness says beforehand which route a publish would take, which checks fail and which steps are off the list (outside_list), and the publish answer names the same steps when it sends a flow to review. The list is checked again at the moment the version goes live. If someone switched it off or narrowed it in the meantime, or a step was added, the version goes to review or is refused; it never goes live outside the list. A version records how it went live (published_via: review, review_off or self_service) and, for a self-publish, which version of the list admitted it. The flow's audit tab shows a self_published entry.
Flows switched off by the September 2026 update. Before May 2026 a flow version could go live without a review, and some of those versions were never reviewed afterwards: they kept running without an approval. The September 2026 update switched them off. Each one has a version_switched_off entry in its flow's audit tab, and a run or schedule of that flow now fails with "This flow has no approved version". Submit the version for review to put it live again.
What a version is approved under. A flow is bound to a governing domain, and to a published data contract when it reads warehouse data (a warehouse query step, saved or inline); a flow that reads none needs no data contract. Every flow needs a published ownership contract before it can go live. When a version is approved, Datahub records which data contract and which ownership contract it was approved under. Changing the flow's binding afterwards does not change what the live version runs under; the new binding applies from the next version you publish. The builder's Governance tab shows the contract the live version was approved under and warns when the binding has moved away from it. While a version is under review, its domain and data contract are locked, because the reviewer is judging it under them: withdraw the review to change them, then submit again.
Versioning, replay, debug¶
- New version at any time — copies the graph from the current active version and becomes the new draft.
- Diff two versions → field-by-field table with semantic colours (added: green, removed: red, changed: yellow).
- Audit log — every definition / version change is recorded with field-level diffs.
- Replay-from-step — open a run → pick a step → Replay from here → a new instance starts at that step on the active version, optionally with overridden context.
- Retry from failure — a failed run can be retried from the failed step; the new instance is linked via
superseded_by. - Debug tab — start a run from any node (engine entry mid-graph), pass overrides, and trace through.
Transform expressions¶
A Transform node works in one of two modes.
Mapping — set an input key and an output key, and the value is copied across unchanged.
Expression — set an expression and an output key, and the expression is evaluated against the flow context, with the result written to the output key.
Inside an expression, the keys already in the context are available as names, so a step that
produced subtotal lets you write subtotal * 1.21. A few rules follow from that:
- Text constants need quotes.
pendingis read as a context key;'pending'is the word. - Nested values use an index, not a dot.
payload["id"], notpayload.id. - No braces.
{{subtotal}}is a message's form; in an expression it is refused. The chips under the expression insert each name the way an expression reads it. - Functions are a fixed list:
upper,lower,len,round,abs,int,float,str,concat,min,max,coalesce. Conditionals use thea if cond else bform. - Anything else is refused. Expressions run in a sandbox with no access to the filesystem, the network, other flows' data or Python's built-ins.
A mistake that can be caught before the flow runs — a syntax error, a dot, an unknown function — is reported when you submit the version for review (and again on activation). Saving a draft does not check it. A name that does not exist in the context can only be known for certain at run time, so that fails the step, with the reason shown on the run. The builder warns earlier, under the expression, about a name no earlier step, flow variable or trigger sets, with the listed name it most likely meant.
A Transform does one of the two, never both: a node carrying an input key and an expression is refused on submit, rather than quietly running the copy and ignoring the expression.
If you built a flow before expressions were evaluated, its output key received the expression's own text. Check those nodes: text you meant literally now needs quotes (
'pending','Order received'); a number now lands as a number rather than the digits as text, which changes how a later Condition compares it; and anything arithmetic-shaped is now computed instead of stored. Only the first of those fails loudly — the other two change the value silently.
Sub-flows¶
Sub-flows let you compose: build small, focused flows (one HTTP call + transform), then have a parent flow invoke them as steps. Cascade is prevented at the engine level (max depth 5; cycle detection in the dependency graph). The Run Detail's Execution tree visualises the parent → child instances so you can navigate between them.
Templates¶
The Templates browser is a library of reusable, importable flows. It is not in the Flows menu for now: open it with Start from a recipe when you create a flow.
- Use — clones a template into a new flow definition in your tenant.
- Publish — promote a flow definition to a template (with category + tags).
- Export / Import JSON — portable envelope for sharing across tenants.
Business recipes¶
Five recipes ship out of the box. A recipe is a ready flow: every step is already in place, and you only make the choices that belong to your organisation, such as the Teams channel, the metric or the team.
| Recipe | Starts on | What it does | You choose |
|---|---|---|---|
| Weekly KPI update | A schedule | Posts a metric's value for this month so far to a Teams channel, next to last month. | A metric and a channel |
| Tell me when a metric drops below a value | A schedule | Checks a metric and posts to a Teams channel only when it is below the value you set. | A metric, the value and a channel |
| Ask for approval, then announce it | A start by hand or over the API | Asks a team to approve, then posts the outcome to a Teams channel. | The team and a channel |
| Tell the team when a review is approved | An approved review | Posts to a Teams channel when a review in Datahub is approved. | A channel |
| Monthly report from a saved query | A schedule | Runs a saved query and posts its row count to a Teams channel. | A saved query and a channel |
Each card says what the recipe does, names its steps and lists what you will need. Use recipe asks for the domain that governs the flow, and for the owner group when more than one of your groups works in that domain. The builder then opens on the go-live panel, which lists each choice that is still open as a step that is not set up.
Once the choices are made, a recipe goes live on your own publish when your organisation has turned on self-service publishing: every step it uses is on the default list. The saved-query recipe reads data, so it first needs a published data contract, like any flow that reads data. The schedule or the event a recipe starts on is set up under the flow's Runs, in Schedules or Event Subscriptions, before its first go-live or after it. Until the flow goes live, a schedule's row says "Starts once the flow goes live." and its slots pass without a run. Go live tells you when a scheduled or event flow has nothing set up to start it, or when everything that would start it is switched off. That doesn't hold up going live, and its button opens the Schedules or Event Subscriptions list.
The New Flow dialog on /flows offers Start from a recipe next to Blank flow, and opens this gallery.
The six starter templates of earlier releases are archived: their steps were a type the engine never ran. An admin still finds them under Archived.
Analytics¶
- KPI strip — active definitions, success rate (rolling), runs today, average duration.
- Timeline — stacked bar chart by day: succeeded / failed. Click a bar to drill into that day's executions.
- Per-definition table — invocations, duration p50 / p95, success rate.
- Step-level table — per-node-type metrics; optional heatmap toggle on the canvas colours nodes by latency.
- Dependency graph — visual map of cross-flow sub-flow references, useful before deactivating a flow others depend on.
Who is told
A failed run tells its flow's owner, and the warning about runs in flight longer than expected waits on /flows for whoever opens it. A flow with no owner, or with an owner who has left, used to tell no one. Every 15 minutes Datahub now counts the runs the /flows warning counts, and the switched-on flows whose Last run is Failed. The Administration page shows both among the checks that need attention, and everyone who holds flow.definitions.manage gets one bell notification for each that opens /flows. The warning and the Last run column there also need flow.instances.read. While it is unread it keeps up with the count, so a cause that persists doesn't ping you every few minutes. When nobody holds flow.definitions.manage, the people in the Administrator organisation role are told instead, or, when there are none, those in the Administrator role group. A stuck run stops counting once it ends or is cancelled, and a failing flow once a later run completes, or once it is switched off or archived. Once nothing is left to fix, the next check takes the notification out of the bell.
Limitations¶
| Limit | Why | Workaround |
|---|---|---|
| Sub-flow depth capped at 5. | Sanity. | Refactor deeply nested flows into siblings. |
| Webhook is per-definition; no per-instance auth. | Header-based shared secret. | Rotate the secret regularly; restrict via a tenant-side allowlist. |
| Loop iterations are capped at 1000 (100 unless you raise it). | A runaway loop would otherwise run until the execution timeout. | Raise the cap on the node. If the list is longer than the cap the loop stops early and records that it was truncated. |
| A Delay resumes at or after its deadline, never before — resumption is checked once a minute. | The scheduler ticks per minute. | Delays shorter than a minute still cost up to a minute. Maximum delay is 30 days. |
| A Run Metric or Warehouse Query step waits up to 10 minutes for a SQL warehouse that is still starting, checked once a minute. A step inside a parallel branch or a sub-flow cannot wait: it fails straight away, naming the step. | A stopped Databricks SQL warehouse takes a few minutes to start. | While it waits, the step shows Waiting for the warehouse, and the time does not count toward the flow's timeout. If the warehouse is still not up after 10 minutes, the step fails: check in Databricks that the warehouse can start, then retry the run. For a parallel branch or sub-flow, retry the run in a few minutes, or move the step onto a single path. |
| The Webhook node cannot call an arbitrary URL. | Outbound traffic is governed: a flow may only reach endpoints registered as approved connections. | Ask an administrator to register the operation; it then appears in the node's picker. |
| A Webhook or Call API step reads at most 10 MB of the answer. Over that, the step fails with "The response from api.example.com exceeded the 10 MB limit on outbound HTTP responses" and keeps none of it. An answer that says up front it is larger is refused before Datahub reads any of it. | An unbounded answer would fill the server's memory. Datahub stops reading as soon as the limit is passed, also for a compressed answer, and only accepts gzip or deflate compression. | Ask the API for less: a filter or a smaller page. The limit is a deployment setting (DATAHUB_OUTBOUND_HTTP_MAX_RESPONSE_MB), so whoever runs your Datahub installation can raise it. |
| Step output cap is 64 KiB. | Storage hygiene. | Stream large payloads to object storage and pass the URL. |
A schedule's cron runs in UTC, not your local time. 0 0 * * * (Daily, hour 00 UTC) fires at 02:00 Amsterdam time in summer and 01:00 in winter, and every local run time moves by an hour when daylight saving time changes. |
Determinism: one clock for every server. | Pick the hour in UTC (the picker's Hour (UTC)), or write a custom cron in UTC. The schedule's Next run line shows when it will fire in your own time zone setting. |
| Pausing a schedule skips the runs it misses; switching it back on waits for the next cron slot, it does not catch up. | A missed occurrence is missed, not owed, so resuming never sends a burst of overdue runs. | Start the flow manually from the builder's Run button if you need the missed run. |
| Replay starts a new instance — it doesn't mutate the original. | Auditable. | The new run's superseded_by link makes the chain visible. |
| A Capability step that references a saved query always runs its active version, never its draft. | The draft is what its owner is still editing, or what a reviewer is looking at — not something a flow should execute mid-change. | Submit or activate the query first; a query that has only ever been a draft is refused, not silently skipped. |
Audit & compliance¶
| Question a CISO might ask | Where to look |
|---|---|
| "Who changed this flow and when?" | Builder → Audit toggle → field-level diff timeline. A domain, data-contract or owner change records the old and new value. |
| "Under which contract was this version approved?" | Builder → Governance tab for the live version; for any version, approved_data_contract_id / approved_ownership_contract_id on the version (API, or the list_flow_versions MCP tool). |
| "Who triggered this run?" | Run detail → triggered_by + trigger source (manual / schedule / webhook / event / sub-flow). |
| "Did this flow leak credentials in the context?" | All step contexts are redacted via SecretRedactor; verify by inspecting any step's input / output. |
| "Where did this webhook call go?" | Action node config + execution log entry shows the resolved URL. |
| "Did this run touch a sub-flow that touched another?" | Run detail → Execution tree. |
| "Can a non-author trigger an active flow?" | Yes if they have flow.definitions.manage; webhook calls require the secret. |
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
| Webhook returns 403 | Wrong / missing X-DataHub-Signature (bad HMAC, tampered body, or wrong secret). |
Recompute HMAC-SHA256 over the exact raw body with the current secret; rotate if leaked. |
| Webhook returns 429 | Rate limit (60 / min). | Throttle the caller. |
| Run stays running forever | Step timed out and the timeout handler crashed (rare); or the engine couldn't compute a UTC-aware duration. | Cancel from the run detail; check execution logs. |
| A step fails with "The SQL warehouse did not start within 10 minutes" | The step waited for its Databricks SQL warehouse to start, and it did not come up in time. | Check in Databricks that the warehouse can start, then retry the run. |
Step input shows _truncated: true |
Payload exceeded 64 KiB. | Reduce the upstream payload or stream to object storage. |
| A Webhook or Call API step fails with "exceeded the 10 MB limit on outbound HTTP responses" | The API answered with more than the outbound response limit. | Ask the API for less (a filter or a smaller page), or have the limit raised (see Limitations). |
A Webhook or Call API step fails with "was compressed as 'br'" (or zstd) |
The API compressed its answer in a way Datahub did not ask for and cannot read within the size limit. | Configure the API to answer uncompressed or with gzip. |
| Replay-from-step errors with "non-step node" | You picked Start, End, or a gateway. | Pick an action / transform / condition / sub-flow node. |
| Activate fails | Config validation on a node failed. | The failing node is highlighted; required fields are red. |
| Saving the canvas, a new version or a submit says "You are not a member of the … domain" | The flow belongs to a domain your role groups do not work in. | Add your role group on the flow's domain, on the domain's Members tab (or ask someone who can), or ask one of its members to make the change. |
| Running a draft, sending it a test webhook or retrying a failed test of it says "you cannot test-run this flow's work in progress" | The flow belongs to a domain your role groups do not work in. A test runs steps no reviewer has approved yet, so it needs the same domain as changing the flow, whatever flows role you hold. | Add your role group on the flow's domain, on the domain's Members tab (or ask someone who can), or ask one of its members to run the test. Running the live flow by hand is not affected. |
| "Submit for Review" fails | Version not in draft, or a capability node names an unregistered capability. | Fix the graph errors shown in the validation panel. |
| Activating directly (review switched off) says "Version 4 is under review, so it can't be activated directly" | Only a draft is activated directly. A version under review is decided by its review, and approving it puts it live. | Let the reviewer decide it. Or have the review withdrawn (by the person who submitted it, the person or team it is assigned to, or a member of the Administrator role group) to make it a draft again, then activate it. |
| Version stuck in "Under Review" | The assigned reviewer hasn't acted yet. | Ask them to approve in the Tasks inbox, or withdraw the review. |
| Your flow stays "Under Review" after the reviewer approved, or comes back rejected with a reason about its bindings | The reviewer's approval was refused because the flow could not go live yet. Only the reviewer sees that refusal when it happens; you see the reason if they reject with it. Most often the ownership contract is not published: it is checked at approval, not at submit. | Get the ownership contract published (or the saved query approved, if that is the reason), then ask the reviewer to approve again; the review stays open meanwhile. A data contract or domain that no longer holds cannot change while the flow is in review, so the reviewer sends it back and you resubmit. |
| A flow that used to run now fails with "This flow has no approved version" | Its live version was never approved. The September 2026 update switched those off (the audit tab shows version_switched_off), or the flow was never activated. |
Submit the version for review. With review switched off, activate it directly. |
| Schedule doesn't fire | Schedule paused, or cron is malformed — or no version of the flow is live, and a schedule only ever runs the live one. | Toggle it on. It then runs at its next cron slot, not straight away. The cron is in UTC, so check the Next run line, which shows it in your time zone. Before a flow's first go-live, a schedule waits and runs from the first slot after it goes live. Once a flow was live and has no live version any more, Add is disabled until it is published again. |
| Publish sent the flow to review when you expected it to go live | Self-service publishing is off, a step is not on the approved list, or the list changed after you checked. | The readiness check and the publish answer name each step off the list and why: not_on_list, inline_query for inline SQL, or query_not_approved for a saved query whose approval rides along in the review. Use a saved query instead of inline SQL, or ask an administrator whether the step belongs on the list. |
| Domain or data-contract picker is greyed out | A version is under review, and its reviewer is judging it under the current binding. | Withdraw the review, change the binding, submit again. |
Arriving from a domain's detail page narrows this list to that domain automatically — a banner names it, with a one-click way back out. See Domains.
See also¶
-
Governed operations (run metric, notify) available inside flows.
-
Run your own Python on the flow's values: the contract, the packages and code runs.
-
Model the human side of the work; Flows executes it.
-
SQL alerts on Databricks that can trigger a flow.
-
Ask "what flows ran today?" and navigate runs conversationally.