Skip to content

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: pre hooks run before the move, in execution_order. The first one that fails blocks the move, and the rest don't run. post hooks run after the move. Every post hook runs, and a failure doesn't undo the move.
  • handler_name: run_script runs a Plane Runner script. send_webhook sends a signed POST to 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 ​

  • id string (uuid)

    Unique identifier for the hook.

  • workflow_transition_id string (uuid)

    The transition the hook runs on.

  • project_id string (uuid)

    The project of a project workflow hook. null for hooks on a catalog workflow.

  • phase string

    When the hook runs, pre (before the move) or post (after it). Fixed at creation.

  • handler_name string

    What the hook does, run_script or send_webhook. Fixed at creation.

  • is_enabled boolean

    Whether the hook runs. A disabled hook stays listed but doesn't run.

  • execution_order integer

    Run order within the phase, starting at 1. Plane sets it on create, one more than the highest in the phase. It isn't writable.

  • config object

    The handler's settings. See Config by handler. The signing secret of a send_webhook hook is never in a read response.

  • created_at string (date-time)

    When the hook was created.

  • updated_at string (date-time)

    When the hook was last updated.

Response200
json
{
  "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"
}
Response201
json
{
  "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_id string (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_variables object: 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 ​

  • url string: the destination. Required on create. Plane rejects destinations it won't deliver to, such as private network addresses.
  • headers array of object: up to 20 extra headers, each with key, value, and is_secret (defaults to true). Keys must be unique, and the comparison ignores case. Host, Content-Length, Content-Type, User-Agent, X-Plane-Delivery, X-Plane-Event, and X-Plane-Signature are 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 matches output_data.delivery_id on the execution.

If the secret leaks or is lost, regenerate it. The old secret stops working at once.

The execution object ​

Attributes ​

  • id string (uuid)

    Unique identifier for the run.

  • work_item_id string (uuid)

    The work item whose move started the run. Nullable.

  • status string

    pending, running, success, or failed.

  • started_at, completed_at string (date-time)

    When the run started and finished. Nullable.

  • input_data object

    What the hook ran with. For send_webhook: url_host, event_type (workflow.transition), and phase. For run_script: the input the script received. It has an event object that names the work item and its state, project, and workspace, and a context object with workflow_transition_id and rule_id.

  • output_data object

    What the run produced. For send_webhook: delivery_id, status_code, response_body (the first 1024 characters), duration_ms, and error. For run_script: what the script returned, or its error data.

  • error_message string

    Why 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 ​

MethodPathDescription
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_type and a masked config.secret, and has no workflow_transition_id, project_id, created_at, or updated_at.
  • Reads leave the signing secret out of config entirely instead of masking it.
  • The hook id path parameter is pk. v1 calls it hook_id.
  • In an ungoverned workspace, every route returns 409 states_managed_at_project. v1 returns 400.

See Migrating from v1 for the full list.