Flow Capabilities: governed operations in automation¶
Invoke platform operations — query a metric, run a saved warehouse query, send a notification — from inside a flow, without writing code or pasting webhook URLs; every call runs through a governed contract that handles credentials, audit logging, and access control.
Where a flowAction node reaches out to an external URL you supply, a flowCapability node reaches in to a Datahub operation the platform already governs — so the flow never sees a token or a raw endpoint.
When to reach for this¶
-
Query a metric on a schedule
Run a governed metric query and branch on the result — e.g. if revenue dropped >10%, notify finance.
-
Read a saved warehouse query
Pull rows from a private or org-shared Insights query via
query_ref— no SQL in the graph. -
Send a governed notification
Post to a pre-configured Microsoft 365 or Google Workspace channel — no embedded URLs or tokens.
-
Read the data catalog
Query a registered data catalog asset through a governed contract — the platform handles access control and audit, and no raw SQL sits in the graph.
-
Chain platform operations
Run a metric, check its result, then send a notification, all through governed contracts.
-
Call a tool on a connected MCP server
Invoke a tool your admin has connected and discovered on an outbound MCP server — no raw endpoint, no embedded token.
When not to reach for a capability
- Calling an external API → use a
flowAction(HTTP) node. - Data mapping or context manipulation → use a
flowTransformnode. - Branching on a value → use a
flowConditionnode.
How a capability runs¶
A flowCapability node names an operation and its inputs. Before the operation executes, the engine checks that the capability is registered and granted by the flow definition, evaluates the governed contract (access control + audit), brokers ephemeral credentials, then runs the platform operation and threads a redacted result back into the flow context.
flowchart LR
NODE["flowCapability node<br/>run_metric · warehouse_query · notify · catalog_query · catalog_snapshot · mcp_tool_call"] --> GRANT{"Registered<br/>and granted?"}
GRANT -->|no| DENY["Rejected<br/>not registered / not granted"]
GRANT -->|yes| GATE(["Governed contract<br/>access control · RBAC · audit"])
GATE --> CRED["Credential broker<br/>ephemeral · never in context"]
CRED --> OP["Platform operation<br/>metric · warehouse · integration channel"]
OP --> RESULT[("Result to flow context<br/>redacted · governance_decision_id")]
classDef accent fill:#ff8400,stroke:#e67700,color:#ffffff,font-weight:600;
class GATE accent
Figure 1 — Two gates precede every operation: the capability must be registered in the engine and granted by the flow's approved set, then the governed contract decides access. Credentials are brokered per-run and never merged into the shared context; the result lands under the node's output key with a governance decision id.
Why teams reach for this
The flow author never handles a credential or a raw endpoint. The platform brokers access per-run, records the governance decision, and redacts the result — so an automation can touch metrics and notification channels without becoming a place secrets leak.
Available capabilities¶
| Capability | What it does | Inputs | Output |
|---|---|---|---|
| Run Metric | Executes a governed metric query against a warehouse integration | Metric, integration, period, compare kind | { current, comparison, delta, metadata } |
| Warehouse Query | Runs saved warehouse SQL via Insights (query_ref) |
Saved query definition | { columns, rows, row_count } |
| Notify | Sends a message through an integration channel (M365 Teams/Outlook, GWS Chat/Gmail) | Integration, channel, subject, body | { status, ok, error_message } |
| Catalog Query | Governed, structured read of a registered data catalog asset (no raw SQL) | Data asset, columns, limit | { columns, rows, row_count, truncated } |
| Call MCP Tool | Calls a tool on a connection your admin has connected and synced to an outbound MCP server | Connection, tool, JSON arguments | { ok, content, structured_content, error_message } |
| Save to table | Saves the rows an earlier step returned into a table asset, as a new version of that table | Table, source step, rows path, allow an empty result | { asset_id, version_id, version_label, row_count, ignored_keys, dry_run } |
Configuring a capability node¶
- In the flow builder, drag a Capability node onto the canvas.
- In the config panel, select the capability type (Run Metric, Warehouse Query, Notify, Catalog Query, Save to table, or Call MCP Tool).
- Fill in the required inputs. The picker only shows assets that exist in Datahub — metrics you've defined, integrations an admin has configured. If the list is empty, define the asset first.
- Set the output key — the context variable name where the result lands. Downstream nodes reference it with
{{output_key}}syntax.
| Field | Required | Description |
|---|---|---|
| Metric | Yes | Select from your governed metric definitions |
| Integration | Yes | The warehouse integration to query through (PAT or service principal only) |
| Period grain | Yes | day, week, month, quarter, or year |
| Period start | Yes | ISO date or {{variable}} from flow context |
| Period end | Yes | ISO date or {{variable}} from flow context |
| Compare kind | Yes | none, previous_period, or yoy_calendar_aligned |
| Output key | Yes | Context key for the result (default: metric_result) |
| Field | Required | Description |
|---|---|---|
| Integration | Yes | The M365 or GWS integration to send through |
| Channel | Yes | The specific channel (Teams webhook, Outlook, Chat webhook, Gmail) |
| Subject | No | Message subject — supports {{variable}} |
| Message body | No | Message body — supports {{variable}} |
| Event type | No | Defaults to flow.capability.notify |
| Output key | Yes | Context key for the result (default: notify_result) |
| Field | Required | Description |
|---|---|---|
| Saved query | Yes | Pick from your private queries or org-shared queries (provisional or governed). Another user's private query never appears in the picker and cannot be referenced. |
| Output key | Yes | Context key for the result (default: wh_result) |
| Field | Required | Description |
|---|---|---|
| Data asset | Yes | Select a registered data catalog asset — the picker only shows assets that exist in Datahub |
| Columns | Yes | Choose which columns of the asset to return |
| Limit | No | Row cap (default: 1000, max: 10000) |
| Output key | Yes | Context key for the result (default: catalog_result) |
| Field | Required | Description |
|---|---|---|
| Connection | Yes | An admin-connected outbound MCP server (Admin → Integrations) |
| Tool | Yes | Must already be discovered on that connection — an admin syncs tools from the connection's settings; a tool the flow author cannot see cannot be picked |
| Arguments | No | Tool call arguments as a JSON object. The tool's input schema is shown read-only as reference; arguments are not validated against it before the call, and {{variable}} substitution only resolves top-level string fields, not values nested inside this JSON object |
| Output key | Yes | Context key for the result (default: mcp_tool_result) |
A tool-reported failure is not a flow failure
If the tool itself reports an error (bad arguments, a downstream issue it caught), the step still completes with { ok: false, error_message } in its result — branch on ok downstream, the same pattern as Notify's DispatchResult. A step that cannot run at all (connection deactivated, tool not in the synced catalog, credential missing) fails the step, the same as every other capability.
| Field | Required | Description |
|---|---|---|
| Source | Yes | The earlier step whose result holds the rows: a connected tool, a catalog or saved query, a Call API step, a code step or a Set a value step, listed by name |
| Table | Yes | A table asset with versioning turned on and a published version. Archived tables, other asset types and tables never published are not listed |
| Rows path | No | Where the rows sit inside the result, such as data.items. Leave it empty for a catalog or saved query's result, or when the result is the list of rows itself or holds only one list |
| Allow an empty result | No | Off by default: a run that finds no rows fails and leaves the table as it was. Turn it on only when an empty table is a right answer |
| Output key | Yes | Context key for the result (default: snapshot_result) |
Each run replaces the rows of the table and publishes them as its new active version. The
version before it is kept, with its own rows, so you can see what changed. A row field the table
has no column for is not kept; the result names those fields in ignored_keys.
Each value is saved in its column's type. A query returns many values as text: a number,
a yes/no or a date in a text form is saved as a number, a yes/no or a date when its column
has that type, and 5.00 fits a whole-number column as 5. A date column keeps only the date
of a date and time. Text is saved without spaces around it, and an empty text is saved as no
value in a column that may be empty. A value that does not fit its column, such as many in a number column or 4.50 in
a whole-number column, fails the run, the error names the row and the column, and the table
keeps its rows.
A test run writes nothing. It checks the table, the rows and the columns as a real run would, and the step then says Test run: 12 rows checked. Nothing was written to the table.
Never self-service
A Save to table step publishes table versions with no review of their own, so a flow that has one is never on the self-service list: it is reviewed before it runs, unless review is switched off for your organisation. The approver sees which tables the flow replaces the rows of.
Setting something up elsewhere, then coming back¶
Some fields link to another page: Go to Metrics when there is no metric yet, Saved queries under the saved query picker, Open query when the chosen query has no active version yet, and Open Connection Governance when an API operation is not approved. These open in the same tab as a side task of the flow, with a slim bar under the header: You're on a side task for Flow "…".
- Unsaved changes on a draft version: the builder asks first. Save draft and go saves them to the draft version, so they are there when you come back. The version that runs now does not change.
- Return reloads the flow (so you see what a colleague saved meanwhile) and brings you back to the same version, with the same step open for editing.
- Done here: creating a metric, or creating or activating a saved query, turns the bar to Done here. On the way back the step picks it, but only when its picker is still empty: a step that already runs a metric or query keeps it, and says so. Save the flow to keep a pick. If the version was submitted for review meanwhile, nothing is picked and the builder says so.
- Open query is about that one query: only activating it counts, not making another one.
- Connection Governance: approval is an admin's, so the bar never says Done here: press Return when you are done.
- Who sees which link: Saved queries and Open query need access to saved queries, Go to Metrics needs the right to define metrics, and Open Connection Governance the right to change what an operation may do. Anyone else is told whom to ask instead of getting a link to a page where they cannot do the task.
- Gone meanwhile: if the flow was deleted or archived, or the step removed from the version, one sentence says so. If the flow cannot be reloaded, the builder says so and offers Try again rather than showing an older copy.
- A new tab: Ctrl-click or Cmd-click opens the page in a new tab as a plain link, with no side task.
Provisional vs governed (org-shared)¶
Warehouse queries carry a visibility that controls where they can be reused:
| Visibility | Meaning | When to use |
|---|---|---|
| Provisional | Light org share — teammates can reuse the SQL in flows, but it is not contract-certified | Exploratory shared queries, drafts under team review |
| Governed | Query is on a Data Product with a published Data Contract | Every flow that is submitted for review: a flow must be bound to a published data contract, and a bound flow's approval accepts governed refs only |
Promotion is one-directional and automatic on demote
Promote a private query to provisional from the builder. Move provisional → governed only after the query is wrapped in a Data Product and linked to a contract. If the last contract is unlinked, the query demotes back to provisional automatically — there is no manual demote button.
Pilot flow: scheduled metric digest¶
A common pattern chains a metric and a notification, entirely through governed operations, with no raw URLs, no embedded credentials, and no code execution.
flowchart LR
T["Schedule trigger<br/>daily at 08:00"] --> M["Run Metric<br/>revenue_mtd · month · yoy_calendar_aligned"]
M --> N(["Notify<br/>finance Teams channel"])
N --> R[("notify_result to context")]
classDef accent fill:#ff8400,stroke:#e67700,color:#ffffff,font-weight:600;
class N accent
Figure 2: On the configured schedule, the Run Metric capability writes metric_result into context, and the Notify capability sends it straight to the Teams channel with {{metric_result}} in the message body.
What this flow actually sends
Run Metric always returns a { current, comparison, delta, metadata } dict, even when the metric has no rows for the period, so this flow sends on every scheduled run: a metric with no data still sends, with empty lists inside the result. The message body is that result as returned, unformatted: {{metric_result}} renders the raw Python dict, including values such as UUID('a1b2c3d4-5678-90ab-cdef-1234567890ab') and datetime.datetime(2026, 9, 14, 8, 0). There is no condition step here, because a flow cannot yet branch on a value inside the result: a flowCondition compares one top-level context value, and {{variable}} templates read top-level names, and neither reaches a number inside metric_result, such as the change for one period. Choose yoy_calendar_aligned as the compare kind, because the metric service refuses none and previous_period. A later release will return the comparison row paired with each current row alongside delta.
Limitations¶
| Limitation | Detail |
|---|---|
| OAuth U2M not supported for Run Metric | Flows execute headlessly (no interactive user), so integrations that require user delegation cannot be used. Use a PAT or service-principal integration instead — selecting an OAuth U2M integration returns a clear error. |
| Only registered capabilities can be invoked | A flow cannot call arbitrary platform internals — the set of capabilities is explicitly registered in the engine. |
| Grant check | Each flow can only invoke capabilities that appear as flowCapability nodes in its graph. A tampered graph cannot invoke undeclared operations. |
| Warehouse query ownership | At save, approval, and run time the platform checks that every query_ref is either owned by the flow's accountable user or org-shared. Foreign private refs are rejected even if pasted into the graph JSON. |
| Contract-bound flows | A flow must be bound to a published data contract before it can be submitted, and a bound flow's warehouse query refs must all be governed (not provisional) before a version can be approved or activated. There is no switch for this: it follows from the binding. |
| Save to table: one table, up to 10,000 rows | A run that returns more rows fails and writes nothing. A run is also refused while the table has a draft version or a version under review: publish or discard the draft, or finish the review, and the next run writes. |
| Save to table: the flow owner's rights | The flow writes as its owner. When the flow is submitted, the platform checks that the owner may edit the table (the datacatalog.assets.write role, and the table's domain when it has one); a flow whose owner may not is refused. |
| Call MCP Tool governance | Governed like Notify: there's no separate per-connection approval step by default — the call is subject to your organization's network posture (permissive orgs allow it, strict/hardened orgs deny by default) unless an admin creates an explicit policy for it in Connection Governance. |
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
| "OAUTH_U2M_UNSUPPORTED" in flow run | Metric uses an oauth_u2m integration |
Switch the metric's integration to PAT or service principal |
| "Capability not registered" | Flow references a capability the engine doesn't know about | Check spelling; available capabilities: run_metric, notify, warehouse_query, catalog_query, catalog_snapshot, mcp_tool_call |
| "Capability not granted" | The version's approved capability set does not include this capability — it was added after approval or the version was not approved with it | Submit a new version with the capability node for approval |
| "Credential resolution failed for capability 'X' through the connection 'Y': This connection has no integration bound" (or "The integration bound to this connection no longer exists") | The connection the step goes out through, as Connection governance lists it, has no working integration to take credentials from | Ask an administrator to bind an integration to that connection in Connection governance (Admin → Connection governance → the connection) |
| Empty metric/integration picker | No governed assets exist yet | Define a metric or configure an integration first |
| Saved query picker empty | No private or org-shared warehouse queries yet | Create a saved query in Insights or ask a teammate to promote one |
| "NOT_REFERENCEABLE" at save or approval | Graph references another user's private query | Pick an org-shared query or one you own |
| "NOT_GOVERNED" at approval | Flow is contract-bound but a ref is only provisional | Promote the query to governed (product + contract) or turn off contract binding |
| Notify status "blocked" | Integration inactive, channel disabled, or capability flag off | Check integration status and channel configuration in Admin → Integrations |
| Call MCP Tool step fails with "not a discovered tool" | The tool name isn't in the connection's synced catalog — it may have been renamed, removed, or never synced | Sync tools on the connection in Admin → Integrations, then re-pick the tool |
| Call MCP Tool step fails with "inactive or deprecated" | An admin deactivated or retired the connection | Reconnect or point the step at an active connection |
| Save to table: "has no rows, so the table was left as it was" | The source step returned an empty list | Check the source step's result. If an empty table is right for this flow, turn on Allow an empty result |
| Save to table: "holds several lists" or "is not a list of rows" | The result is not one list of rows, or the rows sit deeper in it | Set Rows path to the list that holds the rows, such as data.items |
| Save to table: "refused the rows: Record 3: Column 'rating': expected number, got str 'high'" | The value it names, in that row, does not fit the column's type (text in a number column, a fraction in a whole-number column, a word in a yes/no column). "Column 'rating': 'Infinity' is not a number a table can hold" is NaN or Infinity, as a number or as text; an import and a record edit refuse it in the same words. Only the failed step's line in the run's log names the value: the run's error, the owner's task and its notification say the row, the column and what the column holds | Fix the value at its source, or filter the row out in an earlier step. If the column should hold any text, change its type to text in the table's schema |
| Save to table: "has a draft version" or "is under review" | Someone is editing or reviewing the table | Publish or discard the draft, or finish the review; the next run writes |
| Save to table: the table is missing from the picker | It is archived, not a table, has versioning off, or has never been published | Open the table in the catalog and publish a first version with versioning on |
| Tool picker shows "you don't have access" | You can see the connection but lack the separate integrations.mcp_server.read role needed to browse its tools |
Ask an admin to grant that role — it's independent of the role that lets you pick the connection itself |
Approval governance¶
Every flow version must pass through the approval workflow before activation. At approval time, the platform freezes the exact set of capabilities the version declares. At runtime, the engine checks every capability invocation against this frozen set — a flow cannot execute capabilities it was not approved for.
No drift between what was reviewed and what runs
- Adding a new capability node to an already-active flow requires creating a new version and getting it approved.
- The approved capability set is immutable on the active version.
- The temporal record (SCD Type 2) lets auditors and the brain reconstruct which capabilities were active at any point in time.
See also¶
-
The visual automation engine that hosts capability nodes — triggers, versioning, replay, audit.
-
SQL alerts on Databricks that can trigger a flow — the counterpart to a scheduled metric check.
-
Define the governed metric definitions the Run Metric capability queries.
-
Configure the Teams / Outlook channels the Notify capability sends through.