Skip to content

Create a workflow hook ​

POST/api/v2/workspaces/{slug}/projects/{project_id}/workflows/{workflow_id}/state-transitions/{transition_id}/hooks/

Attach a hook to a workflow transition. From then on, Plane runs the hook each time a work item moves along that transition, before the move for a pre hook and after it for a post hook.

Plane assigns execution_order one past the highest existing value for the same phase on this transition. The hook is enabled unless you send "is_enabled": false.

For a send_webhook hook, Plane generates the signing secret and returns it in this response as config.secret_plaintext. No other response returns the secret. Store it now, or rotate it later with Regenerate a workflow hook secret.

Creating a hook adds a transition_rule entry to the workflow's activity.

Path Parameters ​

slug:requiredstring

The workspace slug. It appears in your Plane URLs. In https://app.plane.so/my-team/projects/, the slug is my-team.

project_id:requiredstring

The project the workflow belongs to. Accepts the project UUID or its bare identifier, for example ENG.

workflow_id:requiredstring (uuid)

The workflow the transition belongs to.

transition_id:requiredstring (uuid)

The transition to attach the hook to. Plane checks it before anything else, so an unknown transition returns 404 even when the body is invalid.

Body Parameters ​

phase:requiredstring

When the hook runs. Immutable after creation.

  • pre. Runs before the move. If the hook fails, Plane blocks the transition.
  • post. Runs after the move. Plane records a failure but does not block or undo the move.
handler_name:requiredstring

What the hook does. Immutable after creation.

  • send_webhook. Sends the transition event to config.url, signed with the hook's secret.
  • run_script. Runs the script in config.script_id.

Each handler needs its own plan feature. When the workspace doesn't have it, the request returns 402 payment_required.

config:requiredobject

The handler's settings.

For send_webhook:

  • url string, required. The destination, http or https only. Plane rejects URLs that resolve to private or internal addresses and domains that the instance disallows for webhooks.
  • headers array, optional. Up to 20 objects of the form { "key", "value", "is_secret" }. key is 1 to 256 characters and must be unique without regard to case. value is required and up to 8192 characters. is_secret defaults to true. When it is true, Plane stores the value encrypted. The names host, content-length, content-type, user-agent, x-plane-delivery, x-plane-event, and x-plane-signature are reserved.

Don't send a secret. Plane generates it.

For run_script:

  • script_id string, required. 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.
is_enabled:optionalboolean

Whether the hook runs. Defaults to true.

Response shaping ​

fields:optionalstring

Comma-separated list of fields to return. Unrequested keys are omitted from the response, not returned as null, so absent means "not requested" and null means "actually null". id always comes back whether or not you name it.

Pass all for every requestable field. An unknown name is a 400 that lists the valid set and suggests the closest match, so a typo can't silently cost you the saving.

Requestable here: config, created_at, execution_order, handler_name, id, is_enabled, phase, project_id, updated_at, workflow_transition_id.

See Sparse fields.

Scopes ​

projects.workflows:write

Errors ​

StatusCodeCause
400invalid_requestMissing phase, handler_name, or config, a value outside the enums, a send_webhook URL or header list that fails validation, or a missing or unknown script_id.
401unauthorizedMissing or invalid credentials.
402payment_requiredWorkflows aren't enabled on your plan, or the plan feature for this handler_name isn't enabled for the workspace.
403forbiddenYour role or token scope can't edit this project's workflows.
404not_foundNo such workspace, project, workflow, or transition, including a catalog transition in a governed workspace.
406not_acceptableThe Accept header asks for a representation the API can't produce.
409conflictNot raised by the project hook routes. Listed for parity with the workspace hook routes.
413payload_too_largeThe request body is over the size limit.
415unsupported_media_typeThe Content-Type isn't one this endpoint accepts.
429rate_limitedThrottled. Honor the Retry-After header before retrying.
Create a workflow hook
bash
curl -X POST \
  "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/workflows/9e3f6a2c-7b14-4d8e-a5c1-2f60b8d4e719/state-transitions/5c8d2e71-4a9f-4b36-8e05-d17f3a6b9c42/hooks/" \
  -H "X-Api-Key: $PLANE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "phase": "post",
  "handler_name": "send_webhook",
  "config": {
    "url": "https://hooks.example.com/plane/transitions",
    "headers": [
      { "key": "X-Team", "value": "platform", "is_secret": false }
    ]
  }
}'
Response201
json
{
  "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
      }
    ],
    "secret_plaintext": "plane_wh_4f9c2a7e1b8d4e63a5c0f7d29b1e8a46"
  },
  "created_at": "2026-01-14T09:22:41.478363Z",
  "updated_at": "2026-01-14T09:22:41.478363Z"
}
Response402
json
{
  "type": "payment_required",
  "code": "payment_required",
  "detail": "The send_webhook handler is not enabled for this workspace."
}

Store the secret from this response

config.secret_plaintext appears only here. Every later read of the hook omits the secret. If you lose it, regenerate it. Regenerating invalidates the old secret.