Conditions
A condition is a rule that runs between a tool call and the underlying API. Before Willow forwards the call, the condition can make its own API request to look something up, evaluate a rule against the call's arguments and that response, and block the call when the rule matches.
This is the layer that answers questions guards cannot. A guard inspects the content flowing through a call and scores it. A condition inspects the target of the call and makes a binary decision about it: which Slack channel is being read, whether the Drive file carries a Confidentiality label, which Jira project the issue lands in. Enabling a tool decides whether it exists; a condition decides which invocations of it are allowed.
Conditions are attached per tool. Two tools on the same MCP server can have completely different conditions, and a tool with no conditions runs exactly as before.
Conditions are a beta feature and are hidden until the Conditions flag is enabled for your organization. If you do not see the Conditions row action on the Tools tab, ask your Willow contact to turn the flag on. See Beta Features.
Prerequisites
You need:
- admin access to Willow
- the Conditions beta flag enabled for your organization
- an MCP server with authentication configured and tools synced
Conditions that make their own API call use the MCP server's configured credentials, so authentication has to be working before those conditions can evaluate.
How a condition is evaluated
Every condition runs the same three stages:
- Optional API call. If the condition defines one, Willow calls the upstream API with the MCP server's credentials and exposes the raw JSON body to the rule as
response. This is what lets a condition decide based on data the tool call itself does not carry — a Slack channel's name, a Drive file's labels, a Gmail message's labels. - The rule. A JSONata expression is evaluated. A truthy result means the condition matches.
- The outcome. A match blocks the call and returns the condition's message to the AI client instead of the API response.
Conditions on a tool are evaluated in order and the first one to match wins; the remaining conditions are not evaluated. A blocked call is recorded in Monitor > Logs with the error code TOOL_BLOCKED_BY_CONDITION.
Note the polarity: the rule describes what you want to stop, not what you want to allow. arguments.channel_id in variables.blockedChannelIds reads as "block when the requested channel is in this list". Allow-lists are written by negating the match, which is why the built-in allow-list conditions wrap their rule in $not(...).
When a condition cannot be evaluated
If the API call fails or the rule throws, the call is blocked by default. Conditions usually guard sensitive data, so failing closed is the safer default: a Drive label lookup that times out should not quietly grant access to the file it was meant to protect.
Turn on Allow the call if the condition fails to evaluate (fail open) under Advanced when availability matters more than the guarantee, for example on a condition attached to a high-traffic read tool where a blocked call is worse than an unchecked one.
Open the conditions for a tool
- Go to Build > MCP Servers and open the server.
- Select the Tools tab.
- Open the three-dot menu on the tool's row and select Conditions.
The sheet that opens has two sections: Choose an existing condition, listing the predefined conditions the connector ships, and Applied to this tool, listing what is currently configured. Nothing is persisted until you select Save, which saves every condition on the server, not just the one you edited.
Once a tool has at least one condition, its row shows an amber badge with the condition count. Selecting the badge reopens this sheet, so you can tell at a glance which tools are gated without opening each one.
Add a predefined condition
Predefined conditions are written and maintained by the connector, so you configure values rather than logic. The rule, the API call it depends on, and the default message all come from the connector; you supply the specifics.
- In Choose an existing condition, find the condition you want. Conditions the connector designed for this tool are sorted first and carry a Recommended badge.
- Select Add. The condition appears under Applied to this tool with a Predefined badge.
- Fill in its fields. Required fields are marked with a red asterisk.
- Select Save.
Many fields are searchable pickers rather than free-text boxes. When the connector can enumerate the values, Willow calls the upstream API and offers real choices — actual Slack channels with their #name, actual Drive labels with their titles — so you are not pasting opaque IDs. The picker loads on focus, filters as you type, and shows each selection as a removable chip.
You can still type or paste IDs by hand, which matters when the list is too large to browse or when the lookup fails. If suggestions cannot be loaded, the field says so and stays usable: type an ID and select Add, or paste several separated by commas, whitespace, or newlines.
A predefined condition can only be added to a tool once. The Add button changes to Added and is disabled. To gate the same tool on two different lists, add the condition once and write a custom condition for the second rule, or add the predefined condition to a different tool.
Sharing a condition across tools
A condition lives on the MCP server and records which tools it applies to, so the same condition can gate several tools at once. When it does, the editor warns you that changes affect the other tools too:
This condition also applies to other tools. Changes affect them too.
Removing a shared condition from a tool detaches only that tool; the condition keeps running on the others. It is deleted from the server only when you remove it from the last tool using it.
Built-in condition examples
Five connectors ship predefined conditions today. The two worth studying are Slack and Google Workspace, because between them they cover both shapes a condition can take: matching directly on the call's arguments, and enriching the call with an API lookup first.
Slack: matching on arguments
Block specific channels is the simplest possible condition. get_channel_history already receives the channel as an argument, so no lookup is needed and the rule is a single membership test:
arguments.channel_id in variables.blockedChannelIds
You configure Blocked channels, a channel picker backed by conversations.list. Pick #exec-comp and #security-incidents, and any attempt to read those channels comes back with "Access to this Slack channel is restricted by an organization condition" instead of messages. Every other channel is untouched.
The limitation is that it names channels by ID, so it does not cover channels created after you configured it.
Slack: matching on a name pattern
Block channels by name solves that. Slack tool calls carry a channel ID, not a name, so the condition first calls GET /conversations.list, then resolves the current call's channel name out of the response and glob-matches it:
$matchesAnyGlob(response.channels[id = $$.arguments.channel_id].name, variables.blockedNamePatterns)
You configure Blocked channel name patterns as globs where * is the wildcard, for example *sensitive*, exec-*, *-private. Matching is case-insensitive. Because the channel list is fetched at call time, a channel named exec-comp-2027 created tomorrow is blocked without anyone revisiting the configuration. That is the trade-off against Block specific channels: naming discipline buys you a rule that maintains itself, at the cost of an extra API call per invocation.
Only allow channels by name is the same lookup inverted into an allow-list — every channel is blocked unless its name matches one of your Allowed channel name patterns:
$not($matchesAnyGlob(response.channels[id = $$.arguments.channel_id].name, variables.allowedNamePatterns))
Use this when the safe set is small and enumerable (team-*, *public*) and you want new channels to default to blocked rather than allowed.
| Condition | Configure | Effect |
|---|---|---|
| Block specific channels | Blocked channels (channel picker) | Blocks reads from the selected channels. |
| Block channels by name | Blocked channel name patterns (globs) | Blocks reads from channels whose name matches any pattern. |
| Only allow channels by name | Allowed channel name patterns (globs) | Blocks reads from every channel whose name matches no pattern. |
All three are recommended for get_channel_history and get_thread_replies.
Google Workspace: gating on Drive labels
The Google Workspace conditions are the most useful pattern in practice, because they defer the decision to a classification your organization already maintains in Drive rather than to a list kept in Willow.
Both conditions call GET https://www.googleapis.com/drive/v3/files/{fileId}/listLabels for the file the tool is about to touch, then inspect the labels that came back.
Block sensitive Drive files matches on the label itself:
$count(response.labels[id in $$.variables.sensitiveLabelIds]) > 0
You configure Sensitive labels from a picker populated by the Drive Labels API, so you select labels by their real titles. Any file carrying one of them is refused with "This file is labeled as sensitive and cannot be accessed by this tool."
Block Drive files by label field value goes one level deeper, matching on a selection field's value rather than the presence of the label. This is what you want when a single label carries a spectrum — a Confidentiality label whose value may be Public, Internal, or Confidential. Blocking the whole label would block every classified file; blocking a value blocks only the sensitive tier. The picker lists choices as Label › Field: Value, and the stored value encodes the field and choice as fieldId:choiceId.
| Condition | Configure | Effect |
|---|---|---|
| Block sensitive Drive files | Sensitive labels (label picker) | Blocks files carrying any selected label. |
| Block Drive files by label field value | Sensitive label values (label field picker) | Blocks files whose label field is set to a flagged value. |
Both are recommended for google-drive-get-file-metadata, google-drive-copy-file, and google-drive-convert-to-google-doc.
Both conditions read Drive labels, which requires the drive.labels.readonly scope on the Google Workspace connection. If the scope is missing the lookup fails, and because conditions fail closed by default, every call to the gated tools is blocked. Reconnect the MCP server to grant the scope before you attach these conditions.
Other connectors
| Connector | Condition | Configure | Effect |
|---|---|---|---|
| Gmail | Block messages with restricted labels | Restricted label ids (e.g. SPAM, Label_12345) | Fetches the message's labels and blocks reads of messages carrying a restricted label. Recommended for read-email. |
| Jira | Block restricted projects | Restricted project keys (e.g. SEC, HR) | Blocks calls targeting a restricted project key. Recommended for create-issue and get-issues. |
GitHub ships conditions too, but they all target webhook workflows rather than tool calls. See Conditions on webhook workflows.
Write a custom condition
When no predefined condition fits, select New condition to write your own. A custom condition carries a Custom badge and exposes the full editor.
| Field | Purpose |
|---|---|
| Name | Identifies the condition in the editor and in the default block message. |
| Condition (JSONata, truthy = matches) | The rule. A truthy result blocks the call. |
| Optional API call | A request made before the rule is evaluated, exposed to the rule as response. |
The rule is evaluated against these objects:
| Object | Contents |
|---|---|
arguments | The arguments the AI client passed to the tool. |
settings | The MCP server's configured settings. |
variables | Values you configured, for predefined conditions. Empty for custom conditions. |
response | The body of the optional API call, or undefined when there is none. |
So the simplest useful custom condition is a test on an argument:
arguments.repository = "internal-secrets"
To decide based on something the arguments do not contain, expand Optional API call and turn on Make an API call before evaluating. Set the method and path — relative paths resolve against the server's base URL, or use a full https:// URL — and map the request with JSONata:
| Field | Purpose |
|---|---|
| Params mapping | Fills {placeholder} segments in the path, for example { "id": arguments.id }. |
| Query mapping | Query string parameters. |
| Body mapping | Request body, for POST, PUT, and PATCH. |
A caveat when writing rules: inside a JSONata filter predicate, a bare variables or arguments resolves against the item being tested rather than the root. Reach back to the root with $$, which is why the built-in conditions are written [id in $$.variables.sensitiveLabelIds] rather than [id in variables.sensitiveLabelIds].
Choose what happens on a match
When the condition matches offers three outcomes:
| Option | Intent |
|---|---|
| Block | Reject the tool call. |
| Require approval | Ask the user to approve before the call runs. |
| Warn | Allow the call and record a warning in the audit log. |
Only Block is enforced today. The gateway blocks every matching call regardless of which option you select, so do not rely on Warn to run a condition in observation mode — it will still stop the call. To require human review without a condition, use Require approval on the tool itself, described in Manage Tools.
Set the message
Under Advanced, Message is the text returned to the AI client when the call is blocked. Predefined conditions come with a message already written; custom conditions fall back to This tool call was blocked by the "<name>" condition.
Write it for the model and the person reading its output, since this is all they see in place of the result. Say that a policy blocked the call and, where you can, what to do instead — "Ask the channel owner to share the thread directly" is more useful than "Blocked". Do not restate the rule itself; naming the exact labels or channels you gate on tells the caller how to probe around the condition.
Verify it worked
- The tool's row on the Tools tab shows an amber badge with the expected condition count.
- Calling the tool against a target the condition should block returns your message instead of a result. Use Test Tool from the row menu, described in Manage Tools.
- Calling it against a target that should pass still returns a normal result. Verify this too — a fail-closed condition with a broken API call blocks everything, which looks the same as a working condition until you test the allowed path.
- The blocked call appears in Monitor > Logs with the error code
TOOL_BLOCKED_BY_CONDITION.
Conditions on webhook workflows
The same editor also filters incoming webhook events, on the Webhooks tab of an MCP server, inside a workflow. It requires the Webhooks beta flag. Two differences matter:
- The polarity is reversed. For a webhook, a truthy rule means the workflow runs. Every enabled condition must match, and a workflow with no conditions runs on every matching event.
- The rule sees the event, not a tool call:
headers,body,event, andvariables. The optional API call is not executed for webhook conditions, soresponseis always undefined there.
Connectors ship webhook-specific conditions, which appear under Built-in in the workflow's Add condition menu:
| Connector | Conditions |
|---|---|
| Slack | Only for specific channels, Ignore specific channels, Only for specific users, Ignore specific users |
| GitHub | Only for specific repositories, Ignore specific repositories, Only for specific users, Ignore specific users |
Ignore specific users is the one you will reach for first: it keeps a workflow from reacting to its own bot's messages, which is the usual cause of a webhook loop.