Message content
Content belongs to a (rule, destination) pair. The same Slack channel can receive different wording from two different rules, and one rule can send prose to Slack and JSON to a webhook at the same time.
Each destination a rule delivers to gets a row under Content. Click Edit message on a Slack or Microsoft Teams row, or Edit body on a webhook row.

Chat and webhook destinations
| Destination | Body | Left unedited |
|---|---|---|
| Slack, Microsoft Teams | Plain text, rendered from your template | Built-in message, with separate wording for triggered and resolved |
| Webhook | JSON, rendered from your template | The canonical cq.notification.v1 payload |
Both use Liquid for interpolation, conditionals, and loops.
You can loop over resources and over a resource’s apps, nested up to three levels.
Variable reference
Insight
| Variable | Value |
|---|---|
insight.title | Human-readable insight title |
insight.severity | Severity of the insight |
insight.category | Insight category |
insight.source | Where the insight came from |
insight.url | Link back to the insight |
insight.resource_count | Total live violations for the insight |
insight.id | Insight identifier |
Rule and event
| Variable | Value |
|---|---|
rule.name | Rule name |
rule.id | Rule identifier |
event.status | triggered or resolved |
event.cycle_at | When the evaluation cycle ran, RFC 3339 in UTC |
event.id | Delivery identifier, stable across retries |
Resources
resources is the list of resources this notification is about. resource is an alias for the first entry.
| Variable | Value |
|---|---|
resource.id | Resource identifier |
resource.name | Resource name, falling back to the identifier |
resource.type | Resource type, for example aws_s3_bucket |
resource.account | Cloud account |
resource.region | Cloud region |
resource.cloud | Cloud provider |
resource.owner | Resolved owner, or null |
resource.tags | Tag map — index by key, for example resource.tags.env |
resource.environments | List of environment names |
resource.apps | List of apps, each with id and name |
Policy
Available on insights a policy produced. On any other insight policy is null, so every reference needs a guard:
{% if policy %}{{ policy.name }}{% endif %}| Variable | Value |
|---|---|
policy.name | Policy name |
policy.severity | Policy severity |
policy.domain | Policy domain, for example security |
policy.description | Policy description, empty when unset |
policy.url | Link back to the policy |
policy.id | Policy identifier |
policy.query_name | Name of the saved query behind the policy |
policy.query_url | Link to that query in the SQL Console |
The variable picker in the editor inserts the guard along with the variable.
Envelope
Additional variables that carry information about the notification.
| Variable | Value |
|---|---|
schema_version | Payload contract version, cq.notification.v1 |
_resource_total | This notification’s batch size, before the cap |
_resources_omitted | How many resources the cap left out |
truncated | True when the list was trimmed, by count or by size |
Emitting JSON with the json filter
In a webhook body, interpolation is escaped so a resource name carrying a quote cannot break your JSON. That also means {{ resources }} alone emits an escaped string. Use the json filter for real structures:
{
"text": "{{ insight.title }} on {{ resource.name }}",
"resources": {{ resources | json }},
"tags": {{ resource.tags | json }}
}Guard what can be absent
A reference to a variable that does not exist fails the render, and a failed render fails that one delivery.
resource is omitted, not empty, when resources has no entries — so an unguarded {{ resource.name }} fails the render rather than printing nothing:
{% if resources.size > 0 %}{{ resource.name }}{% else %}All clear{% endif %}A resolved notification does carry its resources but they arrive with only their identifier. name, account, region and the rest are empty, and name falls back to the identifier. Write templates that read well either way.
resource.owner is null on a resource with no ownership row. Liquid renders null as empty, so guard it where an empty value would read badly:
Owner: {{ resource.owner | default: "unassigned" }}One template, both statuses
A rule’s content serves the triggered and the resolved notification. Branch on event.status where the wording has to differ:
{% if event.status == "resolved" %}
Cleared: {{ insight.title }} no longer affects {{ rule.name }} resources.
{% else %}
{{ insight.title }} — {{ _resource_total }} newly affected resources.
{% endif %}Preview
Click Preview in the editor to render your template. The result appears under Rendered output, beside the body. The preview runs the same code path as the save-time validation, so an error here is the error a save would report, with the line and column.

The insight and policy fields render their real values; the resources are examples, so the names you see are not the resources this rule would claim. Use the match preview for that.
The preview renders the triggered status. Read a {% if event.status == "resolved" %} branch by inverting the condition while you check it.
Previews need an insight behind the list. The organization default list offers none.
Limits
| Limit | Value |
|---|---|
| Template size | 32 KB |
| Rendered output | 256 KB |
| Render time | 500 ms |
| Resources in a webhook body | 50 |
| Resources in a chat message | 10 |
If there are too many resources or the total size of the notification body reaches the size limit, the resource lists are truncated. Use the Envelope variables to detect the truncation.
Liquid language limitations
CloudQuery Platform does not support the following:
- ranges and range loops, such as
{% for i in (1..9) %} {% tablerow %}and{% capture %}blocks{% include %}and{% render %}tags
Legacy placeholders
Webhook bodies written before notification rules used flat placeholders on the destination. They keep working in rule content, so a migrated body renders unchanged:
insight_title, insight_category, insight_source, insight_resource_count, insight_url, alert_severity, alert_status, alert_violations, alert_message, query_name, query_url
alert_violations is this notification’s batch size; insight_resource_count is the insight’s total live violations. query_name and query_url are empty on a built-in insight, which has no saved query behind it.
Write new templates against the object graph above. The flat names exist for compatibility.
Next steps
- Webhook receivers — consume and verify the payload
- Notification rules — decide which resources reach which destination
Last updated on