Windmill jobs through mcpgate
A developer guide. How to write Windmill flows that drive your services through the gateway — safely, audited, and (optionally) acting as a specific person rather than a service account.
Why route Windmill through the gateway
You can already point Windmill at every service directly — with the corresponding Jira API key, Slack bot token, Google OAuth client, etc. stored in Windmill's own secret store. Routing through mcpgate is structurally different:
- No service tokens in Windmill. The flow holds only its gateway credential. The Jira / Slack / Google tokens stay where the gateway lives. A compromise of the Windmill runtime does not exfiltrate per-service credentials.
- One identity boundary for governance. PII pseudonymization, write-safety, policy hooks and the action classification (read / write / high-risk) all run at the gateway. Your flow inherits them automatically.
- Audited as the owner. When a flow opts into delegation, every call is attributed to the user the flow is acting for (subject) and the workflow caller (actor). "User created issue via Windmill flow X" is what lands in the audit log, not "Windmill did a thing".
Three ways to configure a job
Pick the path that matches who is writing the flow and where the source of truth lives.
1. Through your AI client — natural language
The gateway exposes Windmill as an MCP tool. From any connected AI client you can ask, in plain language:
"Create a Windmill schedule that runs every weekday at 09:00, pulls open Jira issues assigned to me, posts a summary to my #standup Slack channel, and stops if any issue is in the Blocked state."
The AI reads the gateway's Windmill capabilities, drafts the flow, confirms the destructive bits (creating a schedule, posting to Slack) with you under the standard human-in-the-loop pattern, and commits the result. Runs are attributed to you — the audit log shows the flow was created and executed under your identity, not under "the AI".
This is the fastest path for ad-hoc flows and for non-developers who would otherwise file a ticket.
2. Directly in the Windmill workspace
The workspace itself is the source of truth for what is deployed. Hand-editing in the Windmill UI works for everything — scripts, flows, schedules, app builders — and is the right path when you need the Windmill IDE (debugging, type-checking, the run history view).
To use the gateway from a flow:
- Add the gateway as an MCP client connection in the flow (one-time setup per Windmill instance).
- Call the gateway's tools by name (e.g.
jira_write_actions,slack_write_actions) the same way an AI client would. - If the flow should act as the user who owns / triggered it, add a
gateway-services:block to the job declaration (next section).
3. Job declaration alongside your code (optional pattern)
An optional pattern that scales beyond hand-curated flows: drop a .windmill/job.yml at the root of every repository that should run scheduled / event-driven work, and let a single shared flow in your Windmill workspace read the declaration at runtime and dispatch accordingly. One file in the repo onboards it — no per-repo Windmill flow to write.
This is not something mcpgate ships, and Windmill does not require it; it is a convention you put in place once. The shared reader flow is straightforward (clone the repo, parse .windmill/job.yml, execute the script under the declared gateway-services grant). After it exists, the cost of onboarding a new repository to scheduled automation collapses to one file.
Typical shape:
# .windmill/job.yml
name: nightly-pnl-rollup
schedule: "0 3 * * *" # 03:00 UTC
gateway-services:
- bigquery: [read]
- metabase: [read]
- slack: [write]
script: |
# your code, with the gateway exposed as an MCP client
rows = mcpgate.bigquery_read_actions(action="query", sql="…")
mcpgate.slack_write_actions(action="post_message",
channel="#finance",
text=render(rows))
This is the right path when the flow's logic lives close to the code it operates on — ETL jobs, release helpers, billing rollups, anything where the repo is already the source of truth.
The gateway-services: block — opt into delegation
A flow with no gateway-services: block runs as Windmill's own service-account gateway user. The audit log shows the flow's identity as both subject and actor; everything works, but the call is not attributed to the owner.
A flow that declares gateway-services: opts into the delegation path: it acts as the user who owns / triggered it. The gateway:
- Verifies the flow's caller claim (by calling back to the configured Windmill workspace with the flow's own job token).
- Reads the
gateway-services:declaration. - Mints a least-privilege grant for exactly those services and risk classes, capped by the owner's own access.
- Acts as the owner within the granted scope. Subject + actor land in the audit log.
Risk-class rules in the declaration:
read— grants read actions for the listed service.write— adds non-destructive mutations (create issue, post message, …).high_risk— required for destructive actions (delete, archive, dashboard PUTs). High-risk is never granted automatically — the admin has to enable it for this delegation, on top of the declaration. A flow that asks forhigh_riskwithout the admin grant gets a clean refusal in the audit log, not silent execution.
What the audit log looks like
A delegated call surfaces as a single row with two identities:
- Subject = the user the flow acted for. The action shows up under their history; they can see what was done in their name.
- Actor = the workflow caller (e.g. "Windmill flow nightly-pnl-rollup, instance acme-prod"). The operator can trace which flow drove which action.
A refused call (asked for high-risk without the admin grant, asked for a service not in the declaration, asked for an action outside the user's own scope) is logged with the same shape plus the refusal reason. The flow never silently fails to execute — either the call lands in the audit log as success, or the refusal lands there with the reason.
What the operator has to do first
Before the delegated path works in a Windmill instance, the gateway operator needs:
- The Delegated Access feature flag on (Sidebar → General → Clients).
- The Windmill URL and workspace registered on the same panel.
- An admin grant for any
high_riskdeclarations a flow asks for.
See the Workflow Callers admin reference for the operator-side detail.
What this is not
- Not a way to bypass the gateway's policy layer. A delegated flow runs through every hook, every PII pass, every action-classification check the gateway already applies to interactive callers.
- Not "the workflow has all the user's permissions". A grant is narrower than the user's own scope — only the services and risk classes the
gateway-services:block names, with high-risk gated separately. - Not Windmill-only as a connection model. Any MCP-compatible workflow tool can connect to the gateway and run as its own service-account user. The "act as the owner" path is Windmill-first today; other platforms land as their caller-verifier surface is built.
Related
- Workflow Callers & Delegation — the operator-side reference: enabling the feature, registering the Windmill instance, reviewing and revoking grants.
- API reference — the gateway's tool surface (the same surface a Windmill flow calls).
- Hooks reference — the policy layer that runs on every call, delegated or not.
- Audit Log — where subject + actor land for every delegated call.