How to surface plan policy denial messages in ServiceNow notification policies

Last updated: October 29, 2026

When a Spacelift stack fails, you may want your ServiceNow (or other webhook) incident to display the specific error or policy denial message rather than a generic "Spacelift stack X FAILED" description. This article explains how Spacelift exposes error information depending on where in the run lifecycle the failure occurs, and how to write a notification policy that captures the right message.

Understanding when error messages are available

There are two distinct failure scenarios, and each exposes error information differently:

  • Terraform/OpenTofu plan-phase failure (e.g., a lifecycle.precondition check): The run fails before any plan policy is evaluated. Structured fields like error_message, terraform_error, or failure_reason are not populated on the run object. The raw error text is only available in the planning logs via the spacelift::logs::planning placeholder.

  • Plan policy denial: The run reaches the planning stage and a plan policy explicitly denies it. In this case, the denial message is available as a clean string in input.run_updated.plan_policy_decision.deny — no log parsing required.

Policy attachment requirements

Before writing your notification policy, keep these scoping rules in mind:

  • Plan policies must be attached directly to the stack (under the stack's Policies tab). They are not inherited from spaces.

  • Notification policies live at the space level. If your notification policy is in a different space than your stack, it will not be triggered by that stack's runs. Make sure both are in the same space, or move the policy accordingly.

Notification policy that handles both scenarios

The following notification policy dynamically sets short_description to the plan policy denial message when one exists, and falls back to a generic stack-name-based message otherwise. The full planning log is included in the description field for additional context.

package spacelift

import future.keywords.if
import future.keywords.in

run_url := sprintf(
    "https://%s.app.spacelift.io/stack/%s/run/%s",
    [input.account.name, input.run_updated.stack.id, input.run_updated.run.id],
)

failed {
    input.run_updated != null
    input.run_updated.run.type == "TRACKED"
    input.run_updated.run.state == "FAILED"
}

short_desc = msg {
    count(input.run_updated.plan_policy_decision.deny) > 0
    msg := concat("; ", input.run_updated.plan_policy_decision.deny)
} else = msg {

webhook contains {
    "endpoint_id": "your-webhook-endpoint-id",
    "method": "POST",
    "headers": {
        "Content-Type": "application/json",
        "Accept": "application/json",
        "Authorization": "Basic <your-auth-token>",
    },
    "payload": {
        "status": "FAILED",
        "short_description": short_desc,
        "description": "spacelift::logs::planning",
        "assignment_group": "Your Assignment Group",
        "u_stack": input.run_updated.stack.name,
        "u_run_url": run_url,
    },
} if {
    failed
}

sample {
    true
}

Replace your-webhook-endpoint-id, the Authorization value, and assignment_group with your actual values before deploying.

How the short_description logic works

  • If the run was denied by a plan policy, input.run_updated.plan_policy_decision.deny will contain the denial message (e.g., "User already has a Cloud PC. Cannot create another."). Because deny is a set and sets in Rego are unordered, the policy joins all denial messages into a single string rather than picking one.

  • If no plan policy denial exists (e.g., the run failed during Terraform planning before any policy ran), the policy falls back to a dynamic message: "Spacelift stack <stack-name> FAILED".

Note: The spacelift::logs::planning placeholder in the description field will include the full raw planning log output, which may be more verbose than a targeted error message. It is best used for the description (long text) field rather than short_description.

Debugging your policy with the Policy Workbench

You can test your notification policy using sampled inputs from the Policies section in Spacelift:

  1. Navigate to Policies and open your notification policy.

  2. Click the Samples tab.

  3. Find the relevant sample and click Show details to view the input data.

  4. Use that input to test your policy logic in the policy workbench.

Image of a software policy editor UI showing a list of policy samples with outcomes like APPROVE and UNDECIDED.

If the Samples tab shows 0 evaluation records, it means the policy has not yet been evaluated against a run that reached the planning stage successfully.