Workflow hooks overview
A workflow hook is a side effect attached to one workflow transition. When a work item moves along that transition, Plane runs the hook's handler, either before the move or after it.
There are two handlers:
send_webhooksends the transition event to a URL you own, signed with a secret Plane generates.run_scriptruns a Plane script with the event as input.
A hook's phase decides when it runs. A pre hook runs before the move, and a failure blocks the transition. A post hook runs after the move, and a failure does not undo or block it.
The workflow hook object
Attributes
idstring (uuid)Unique identifier for the hook.
workflow_transition_idstring (uuid)The transition the hook is attached to.
project_idstring (uuid)The project the hook belongs to. Always set on these project routes. Hooks on catalog transitions in a governed workspace have
nullhere.phasestringWhen the hook runs.
preruns before the move and can block it.postruns after the move. Set at create time and immutable.handler_namestringWhat the hook does. One of
send_webhookorrun_script. Set at create time and immutable.is_enabledbooleanWhether the hook runs. A disabled hook stays attached to the transition but is skipped.
execution_orderintegerPosition of the hook among the hooks of the same phase on this transition. Plane assigns it at create time, one past the highest existing value for that phase. It is not writable.
configobjectThe handler's settings. For
send_webhook, it holdsurlandheaders. Forrun_script, it holdsscript_idandexecution_variables. The webhook signing secret is never part of a read. See The config object.created_atstring (date-time)When the hook was created.
updated_atstring (date-time)When the hook was last updated.
{
"id": "3b7e9a15-6c2d-4f84-9b31-0e5d8c7a2f69",
"workflow_transition_id": "5c8d2e71-4a9f-4b36-8e05-d17f3a6b9c42",
"project_id": "4af68566-94a4-4eb3-94aa-50dc9427067b",
"phase": "post",
"handler_name": "send_webhook",
"is_enabled": true,
"execution_order": 1,
"config": {
"url": "https://hooks.example.com/plane/transitions",
"headers": [
{
"key": "X-Team",
"value": "platform",
"is_secret": false
}
]
},
"created_at": "2026-01-14T09:22:41.478363Z",
"updated_at": "2026-01-14T09:22:41.478363Z"
}The config object
The shape of config depends on handler_name.
send_webhook
| Key | Type | Notes |
|---|---|---|
url | string | Required. The destination, http or https only. Plane rejects URLs that resolve to private or internal addresses and any domain the instance disallows for webhooks. |
headers | array | Optional. Up to 20 extra request headers, each { "key", "value", "is_secret" }. key is 1 to 256 characters and unique without regard to case. value is up to 8192 characters. is_secret defaults to true. |
These header names are reserved and rejected: host, content-length, content-type, user-agent, x-plane-delivery, x-plane-event, and x-plane-signature.
Plane stores the value of a secret header encrypted. Reads return the stored encrypted string, not the value you sent, so keep the original on your side if you need it.
run_script
| Key | Type | Notes |
|---|---|---|
script_id | string | Required on create. A system script, or a script in this workspace that is either workspace-wide or belongs to this project. |
execution_variables | object | Optional. Variables passed to the script when it runs. |
The webhook secret
When you create a send_webhook hook, Plane generates a signing secret of the form plane_wh_ followed by 32 hex characters. The create response is the only read that carries it, as config.secret_plaintext. Every later read omits it.
Each delivery is a POST with a JSON body and these headers:
X-Plane-Signature, the HMAC-SHA256 hex digest of the body computed with the secret. Recompute it on your side to verify that the request came from Plane.X-Plane-Event, the event type,workflow.transition.X-Plane-Delivery, a unique id for the delivery.
If you lose the secret, or need to rotate it, call Regenerate a workflow hook secret. The old secret stops validating immediately.
Execution history
Plane records every run of a hook. List workflow hook executions returns the runs with their status, input, and output. Deleting a hook does not delete its history.
Entitlements
Each handler has its own plan feature. Creating a hook, or updating an existing one, returns 402 payment_required when the handler's feature isn't enabled for the workspace. The check runs on update too, so a hook created before a downgrade can't be edited afterward.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v2/workspaces/{slug}/projects/{project_id}/workflows/{workflow_id}/state-transitions/{transition_id}/hooks/ | List workflow hooks |
GET | /api/v2/workspaces/{slug}/projects/{project_id}/workflows/{workflow_id}/state-transitions/{transition_id}/hooks/{pk}/ | Get a workflow hook |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/workflows/{workflow_id}/state-transitions/{transition_id}/hooks/ | Create a workflow hook |
PATCH | /api/v2/workspaces/{slug}/projects/{project_id}/workflows/{workflow_id}/state-transitions/{transition_id}/hooks/{pk}/ | Update a workflow hook |
DELETE | /api/v2/workspaces/{slug}/projects/{project_id}/workflows/{workflow_id}/state-transitions/{transition_id}/hooks/{pk}/ | Delete a workflow hook |
GET | /api/v2/workspaces/{slug}/projects/{project_id}/workflows/{workflow_id}/state-transitions/{transition_id}/hooks/{pk}/executions/ | List workflow hook executions |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/workflows/{workflow_id}/state-transitions/{transition_id}/hooks/{pk}/regenerate-webhook-secret/ | Regenerate a workflow hook secret |
Governed workspaces
In a workspace with workspace governance turned on, workflows and their transitions live in the workspace catalog and have no project. These project routes look the transition up by project, so they return 404 there. Manage hooks on catalog transitions with Workspace workflow hooks. Those routes use the same object and the same rules. They return 409 states_managed_at_project in an ungoverned workspace.
Activity
Creating, updating, or deleting a hook adds an entry to the workflow's audit trail with field set to transition_rule. See Workflow activities.
Response shaping
Every hook read accepts ?fields= for sparse responses. See Sparse fields. Errors follow the shared problem+json contract.

