New to Kestra?
Use blueprints to kickstart your first workflows.
Pause for human approval, check the decision explicitly, then purge rows and record the result in one atomic Postgres statement. Webhook or manual start.
Executor of the governed MCP actions pattern. It is started by mcp-governed-action-request, which also documents setup, secrets and the security model: read it first. Companion flow: mcp-governed-action-status.
resolve_key takes the key from the key input or from {"key": "..."} in a webhook body.claim_request moves the row from claimed to pending (the environment needs approval) or running (it does not) in one UPDATE ... RETURNING. It only matches a key that fits ^[A-Za-z0-9_-]{1,64}$ (checked in SQL, so a webhook body cannot bypass it) and a row with no executor yet, or this one. Zero rows means nothing to do.notify_approvers posts a Slack message with a link to this execution (3 attempts). If all fail, the row becomes pending_unnotified, the approval keeps waiting, and the execution ends with a WARNING.wait_for_approval pauses for 1 hour (behavior: FAIL). The decision defaults to deny.authorization_gate runs the purge only for an explicit approve, or when no approval is required. purge_and_record is one SQL statement: the DELETE and the update to executed commit or roll back together, and both require the row to be running for this execution.pending or pending_unnotified becomes expired, running becomes failed.deny, and a purge that only runs for an explicit approve.executed, rejected, expired or failed) and labels on the execution (environment, decision, decided_by).claimed.Teams that run the request flow and need to know what happens after an agent asks for a production action. It also works on its own for any claimed row, started by hand or by its webhook, if you want the approval-then-purge half without the MCP tool.
A shell script can delete rows, but it cannot wait an hour for a person without holding anything open, keep the decision next to the execution, retry a notification, or turn a failure into a recorded status. Here the Pause holds the approval, the errors block records expired or failed, labels show who decided what, and the purge only deletes while the row is running for this execution, so a Restart after a recorded failure deletes nothing.
On Kestra 2.0.4 OSS with Postgres 16, as a least-privilege role, with a local Slack stub.
pending_unnotified and approving still works.expired with nothing deleted.claimed row is recovered by a direct run or a Restart.DELETE.mcp-governed-action-request (public.governed_requests and <schema>.demo_events).PG_PASSWORD (database password), SLACK_WEBHOOK_URL (Slack incoming webhook) and GOVERNED_EXECUTOR_WEBHOOK_KEY (webhook key, a long random value).variables: jdbc_url, jdbc_user and kestra_url. Change kestra_url to your own Kestra URL: it is a placeholder (http://localhost:8080), and the approval link in the Slack message is built from it because kestra.url is unset in Kestra 2.0.4. Left as is, the link points to localhost.mcp-governed-action-request and mcp-governed-action-status, all into the same namespace.INSERT INTO public.governed_requests (request_key, environment, days, status) VALUES ('demo-1', 'staging', 30, 'claimed');key set to demo-1. A staging row runs the purge at once. To see the approval, insert a second row with a new key and production (for example demo-2) and run the flow with that key: the execution pauses. Open it from the Slack link, or from the Executions list, and choose approve or deny.Ideas, not tested in this blueprint:
DELETE in purge_and_record with your own action and keep its guards.pauseDuration. It has to stay a literal: templated Pause settings are ignored in Kestra 2.0.4.notify_slack inside notify_approvers with your own channel or ticket system. Keep that block's allowFailure and record_unnotified so a failed notification stays visible.key (STRING, optional, ^[A-Za-z0-9_-]{1,64}$): request key of a row created by mcp-governed-action-request. Environment and days are read from the row, never from inputs. A webhook run passes it as {"key": "..."}.claimed request, so the id returned to the agent may then point to a no-op execution. The webhook needs no Kestra credentials: its key, read from the GOVERNED_EXECUTOR_WEBHOOK_KEY secret, is the only protection.pending_unnotified, nobody was told. Nothing re-sends the link on its own: calling governed_action_status re-sends it once the row has been pending_unnotified for 10 minutes.pending, pending_unnotified or running. running means the purge statement had not committed (it is atomic). After checking in the Executions list that the executor is really gone, reset it: UPDATE public.governed_requests SET status = 'failed', updated_at = now() WHERE request_key = '<key>' AND status IN ('pending', 'pending_unnotified', 'running'); The key can then be requested again with the same environment and days.claimed: if no executor was started, run this flow by hand with the key, call its webhook, or Restart the failed request execution. If an executor was started but failed before claiming, Restart that executor execution: the claim only accepts the executor recorded on the row.claimed row and does nothing. A Restart re-runs the failed task. After a recorded failure the purge deletes nothing and the execution fails with "Nothing was deleted". If recording failed also failed (for example the database was down), the row stays running and a Restart can re-run the purge with the old approval.This blueprint was created by daiwangk.
