Workspace workflow hooks overview
A hook attaches a side effect to one transition of a catalog workflow. It runs a script or sends a webhook before or after a work item makes the move. In the Plane app, hooks are the pre-validation and post-action conditions on a transition.
Two settings define a hook, and neither can change after creation:
phase:prehooks run before the move, inexecution_order. The first one that fails blocks the move, and the rest don't run.posthooks run after the move. Everyposthook runs, and a failure doesn't undo the move.handler_name:run_scriptruns a Plane Runner script.send_webhooksends a signedPOSTto a URL.
Only enabled hooks run. Plane records each run as an execution and keeps the record after you delete the hook.
These endpoints work only in a workspace with governance turned on. In an ungoverned workspace, they return 409 states_managed_at_project. Project workflows there have the same resource, with the same objects and rules, under workflow hooks.
Learn more about transition conditions
The workspace workflow hook object
Attributes
idstring (uuid)Unique identifier for the hook.
workflow_transition_idstring (uuid)The transition the hook runs on.
project_idstring (uuid)The project of a project workflow hook.
nullfor hooks on a catalog workflow.phasestringWhen the hook runs,
pre(before the move) orpost(after it). Fixed at creation.handler_namestringWhat the hook does,
run_scriptorsend_webhook. Fixed at creation.is_enabledbooleanWhether the hook runs. A disabled hook stays listed but doesn't run.
execution_orderintegerRun order within the phase, starting at 1. Plane sets it on create, one more than the highest in the phase. It isn't writable.
configobjectThe handler's settings. See Config by handler. The signing secret of a
send_webhookhook is never in a read response.created_atstring (date-time)When the hook was created.
updated_atstring (date-time)When the hook was last updated.
{
"id": "e4b7c9a2-3d1f-4e68-8a52-9f0c6b3d7e15",
"workflow_transition_id": "d7a3f5c9-0b2e-4d81-b6f4-9e1c3a5d7b20",
"project_id": null,
"phase": "post",
"handler_name": "send_webhook",
"is_enabled": true,
"execution_order": 1,
"config": {
"url": "https://hooks.example.com/plane",
"headers": [
{
"key": "X-Team",
"value": "platform",
"is_secret": false
}
]
},
"created_at": "2026-01-15T11:04:52.660219Z",
"updated_at": "2026-01-15T11:04:52.660219Z"
}{
"id": "e4b7c9a2-3d1f-4e68-8a52-9f0c6b3d7e15",
"workflow_transition_id": "d7a3f5c9-0b2e-4d81-b6f4-9e1c3a5d7b20",
"project_id": null,
"phase": "post",
"handler_name": "send_webhook",
"is_enabled": true,
"execution_order": 1,
"config": {
"url": "https://hooks.example.com/plane",
"headers": [
{
"key": "X-Team",
"value": "platform",
"is_secret": false
}
],
"secret_plaintext": "plane_wh_4f9a2c7e1b8d4e3fa6c05d9b2e7f7c1d"
},
"created_at": "2026-01-15T11:04:52.660219Z",
"updated_at": "2026-01-15T11:04:52.660219Z"
}Config by handler
run_script
script_idstring (uuid): the script to run. Required on create. A catalog hook can use a system script or a script in this workspace that isn't tied to a project.execution_variablesobject: values passed to the script. Optional.
A pre script blocks the move by failing, or by returning {"success": false, "message": "..."}. The message becomes the run's error_message. On update, the keys you send merge onto the stored config, so a config without script_id keeps the current script.
send_webhook
urlstring: the destination. Required on create. Plane rejects destinations it won't deliver to, such as private network addresses.headersarray of object: up to 20 extra headers, each withkey,value, andis_secret(defaults totrue). Keys must be unique, and the comparison ignores case.Host,Content-Length,Content-Type,User-Agent,X-Plane-Delivery,X-Plane-Event, andX-Plane-Signatureare reserved.
Reads return url and headers. A secret header's value comes back encrypted, never as plaintext. On update, an omitted url or headers keeps the stored value, and a secret header sent without value keeps its stored value.
Plane sends a JSON body with event_type (workflow.transition), entity_type (issue), entity_id (the work item id), workspace_id, project_id, and payload. payload.data describes the work item, and payload.transition has the transition's id, from_state_id (the source chain entry's id), to_state_id, phase, and occurred_at. Plane waits up to 30 seconds for a response and doesn't follow redirects. Any status outside 2xx counts as a failure. For a pre hook, a failure blocks the move.
The signing secret
Plane generates a signing secret for every send_webhook hook. The create response returns it once, as config.secret_plaintext. No read returns it again.
Every delivery carries these headers:
X-Plane-Signature: the hex-encoded HMAC-SHA256 of the raw request body, keyed with the secret. Compute the same value on your side and compare.X-Plane-Event:workflow.transition.X-Plane-Delivery: a unique id for the delivery. It matchesoutput_data.delivery_idon the execution.
If the secret leaks or is lost, regenerate it. The old secret stops working at once.
The execution object
Attributes
idstring (uuid)Unique identifier for the run.
work_item_idstring (uuid)The work item whose move started the run. Nullable.
statusstringpending,running,success, orfailed.started_at,completed_atstring (date-time)When the run started and finished. Nullable.
input_dataobjectWhat the hook ran with. For
send_webhook:url_host,event_type(workflow.transition), andphase. Forrun_script: the input the script received. It has aneventobject that names the work item and its state, project, and workspace, and acontextobject withworkflow_transition_idandrule_id.output_dataobjectWhat the run produced. For
send_webhook:delivery_id,status_code,response_body(the first 1024 characters),duration_ms, anderror. Forrun_script: what the script returned, or its error data.error_messagestringWhy the run failed. Empty when it succeeded.
Plan features
Every hook endpoint needs the Workflows plan feature. Creating or updating a hook also needs the handler's own feature: Plane Runner for run_script, Webhook transition conditions for send_webhook. Updates check the feature too, so you can't edit a hook whose feature has left your plan. Without the required feature, the request returns 402 payment_required.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v2/workspaces/{slug}/workflows/{workflow_id}/state-transitions/{transition_id}/hooks/ | List workspace workflow hooks |
GET | /api/v2/workspaces/{slug}/workflows/{workflow_id}/state-transitions/{transition_id}/hooks/{pk}/ | Get a workspace workflow hook |
POST | /api/v2/workspaces/{slug}/workflows/{workflow_id}/state-transitions/{transition_id}/hooks/ | Create a workspace workflow hook |
PATCH | /api/v2/workspaces/{slug}/workflows/{workflow_id}/state-transitions/{transition_id}/hooks/{pk}/ | Update a workspace workflow hook |
DELETE | /api/v2/workspaces/{slug}/workflows/{workflow_id}/state-transitions/{transition_id}/hooks/{pk}/ | Delete a workspace workflow hook |
GET | /api/v2/workspaces/{slug}/workflows/{workflow_id}/state-transitions/{transition_id}/hooks/{pk}/executions/ | List workspace workflow hook executions |
POST | /api/v2/workspaces/{slug}/workflows/{workflow_id}/state-transitions/{transition_id}/hooks/{pk}/regenerate-webhook-secret/ | Regenerate a workspace workflow hook secret |
Response shaping
Every endpoint that returns hooks or executions accepts ?fields= for sparse responses. See Sparse fields. Errors follow the shared problem+json contract.
Changed from v1
- Every response uses one shape, the hook object above. v1 returns
rule_typeand a maskedconfig.secret, and has noworkflow_transition_id,project_id,created_at, orupdated_at. - Reads leave the signing secret out of
configentirely instead of masking it. - The hook id path parameter is
pk. v1 calls ithook_id. - In an ungoverned workspace, every route returns
409 states_managed_at_project. v1 returns400.
See Migrating from v1 for the full list.

