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. Update, delete, and regenerate-secret look the hook up first, so an unknown id in their path returns 404 instead. 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, http or https only. Required on create. Plane rejects URLs that resolve to private or internal addresses and any domain the instance disallows for webhooks.
  • headers array of object: up to 20 extra headers, each with key (1 to 256 characters), value (up to 8192 characters), 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, the routes return 409 states_managed_at_project. v1 returns 400. Update, delete, and regenerate-secret return 404 first for an unknown id.

See Migrating from v1 for the changes across the rest of the API.