From 0b968cdbe11ade0dc2ce2fcc69f04850457e24d6 Mon Sep 17 00:00:00 2001 From: raj Date: Tue, 16 Jun 2026 13:07:40 +0530 Subject: [PATCH] docs: add calculated policies training materials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add training/calc-policies/ — a 2-hour training runbook, an authoring guide, a simple→complex worked-example ladder, and a README index for authoring Guardrails calculated policies. Co-Authored-By: Claude Opus 4.8 (1M context) --- training/calc-policies/AUTHORING-GUIDE.md | 210 +++++++++++++++++++ training/calc-policies/EXAMPLES.md | 222 +++++++++++++++++++++ training/calc-policies/README.md | 34 ++++ training/calc-policies/TRAINING-RUNBOOK.md | 164 +++++++++++++++ 4 files changed, 630 insertions(+) create mode 100644 training/calc-policies/AUTHORING-GUIDE.md create mode 100644 training/calc-policies/EXAMPLES.md create mode 100644 training/calc-policies/README.md create mode 100644 training/calc-policies/TRAINING-RUNBOOK.md diff --git a/training/calc-policies/AUTHORING-GUIDE.md b/training/calc-policies/AUTHORING-GUIDE.md new file mode 100644 index 00000000..be0fe8da --- /dev/null +++ b/training/calc-policies/AUTHORING-GUIDE.md @@ -0,0 +1,210 @@ +# Calculated Policy Authoring Guide + +A reference to keep open while writing calculated policies in Turbot Guardrails. +Pairs with the worked examples in [`EXAMPLES.md`](./EXAMPLES.md) and the +[`calc-policy` 7-minute lab](https://turbot.com/guardrails/docs/getting-started/7-minute-labs/calc-policy). + +--- + +## 1. What a calculated policy is + +Most policy settings take a **static value**. A **calculated** setting computes +its value at runtime from data in the Guardrails CMDB. It is a two-stage pipeline: + +``` +CMDB ──(GraphQL input query)──▶ JSON data ──(Nunjucks template)──▶ policy value +``` + +| Stage | Console label | Terraform attribute | Purpose | +| ----- | ------------- | ------------------- | ------- | +| 1 | Step 2: Query data using GraphQL | `template_input` | Pull the data you need | +| 2 | Step 3: Transform using Jinja2 Template | `template` | Shape it into the value the policy type expects | + +**Any policy can be calculated.** The value is recomputed automatically whenever +the underlying CMDB data changes. + +--- + +## 2. The GraphQL input query + +The query language is a **super-set** of the Guardrails GraphQL API. Two things +make it special inside a calc policy: + +### Context pivots (no IDs required) + +The query is evaluated *in the context of the resource being governed*: + +- `resource { ... }` / the resource alias (`bucket`, `function`, `vpc`, …) → **this** resource. +- `account { ... }` → the AWS account / Azure subscription / GCP project above it. +- `region { ... }` → the region this resource lives in. +- `folder { ... }` → the folder above it in the hierarchy. + +```graphql +{ + region { Name } + bucket { Name tags } +} +``` + +### `get(path: "...")` — the escape hatch + +Some attributes exist in the CMDB but aren't in the schema. Read them with `get`: + +```graphql +{ + bucket { + tags + grantee: get(path: "Acl.Grants[0].Grantee") + } +} +``` + +### Aliases + +Alias a node to control the name you reference in the template: + +```graphql +{ item: function { tags: get(path: "Tags") } } # → $.item.tags +``` + +### `descendants` / relatives (for aggregation) + +Pull related resources to count or inspect them: + +```graphql +{ + resource { + VpcId: get(path: "VpcId") + descendants(filter: "resourceTypeId:tmod:@turbot/aws-vpc-connect#/resource/types/transitGatewayAttachment level:self,descendant") { + items { VpcId: get(path: "VpcId") } + } + } +} +``` + +> **Tip:** discover the schema via auto-complete in the console builder, the +> **Explore** tab on a resource, or the **Inspect** tab of the +> [mod docs on the Hub](https://hub.guardrails.turbot.com/#mods). + +--- + +## 3. The Nunjucks template + +[Nunjucks](https://mozilla.github.io/nunjucks/templating.html) is Jinja2-style +templating. The query result is the root object, addressed with `$.`. + +```nunjucks +{{ $.bucket.tags['Department'] }} +{% if $.resource.metadata.createdBy %}...{% endif %} +{% for key in inputTagKeys %}...{% endfor %} +``` + +### Output formats by policy-type family + +Calc policies must emit exactly what the target policy type's schema expects. + +**A. Tags `*Template` policies** — emit a `key: value` map (YAML or JSON), or `[]` when empty: + +```nunjucks +{% set tags_plan = {} -%} +{%- if $.resource.metadata.createdBy -%} + {%- set tags_plan = setAttribute(tags_plan, "creator", $.resource.metadata.createdBy) -%} +{%- endif -%} +{%- if tags_plan | length < 1 -%}[]{%- else -%}{{ tags_plan | json }}{%- endif -%} +``` + +**B. Approved / Check `*Custom` policies** — emit a `{ title, result, message }` object via `{{ data | json }}`: + +```nunjucks +{%- if condition -%} + {% set data = { "title": "...", "result": "Approved", "message": "..." } -%} +{%- elif other -%} + {% set data = { "title": "...", "result": "Not approved", "message": "..." } -%} +{%- else -%} + {% set data = { "title": "...", "result": "Skip", "message": "No data yet" } -%} +{%- endif %} +{{ data | json }} +``` + +Common `result` values: `Approved`, `Not approved`, `Skip`. Always include a +`Skip` branch for "no data yet" so the control doesn't flap before discovery. + +### Useful filters & helpers + +| Helper | Use | +| ------ | --- | +| `| json` | render an object as JSON (required for `*Custom` output) | +| `| length` | count items / map keys | +| `setAttribute(obj, key, val)` | add a key to an object immutably | +| `.split('T')[0]` | trim an ISO timestamp to a date | +| `value in ['a','b']` | membership test in conditionals | + +--- + +## 4. Whitespace control (read this before you fight the renderer) + +`{%- ... -%}` trims whitespace around a tag. Calc policy output is validated +against the policy type's schema, and stray blank lines or indentation break +YAML/JSON. The repo convention: + +- Use `-%}` and `{%-` on logic-only lines so they produce **no** output. +- Keep the single value-producing line (`{{ data | json }}`) clean. + +--- + +## 5. Static vs calculated, in Terraform + +A calc policy is just a `turbot_policy_setting` with `template_input` + `template` +instead of `value`: + +```hcl +# static +resource "turbot_policy_setting" "tags" { + resource = turbot_policy_pack.main.id + type = "tmod:@turbot/aws-s3#/policy/types/bucketTags" + value = "Check: Tags are correct" +} + +# calculated +resource "turbot_policy_setting" "tags_template" { + resource = turbot_policy_pack.main.id + type = "tmod:@turbot/aws-s3#/policy/types/bucketTagsTemplate" + template_input = <<-EOT + { resource { metadata } } + EOT + template = <<-EOT + ...nunjucks... + EOT +} +``` + +A working pack typically pairs the **enforcement** policy (`...Approved`, +`...Tags`) with its **calculated input** policy (`...ApprovedCustom`, +`...TagsTemplate`). + +--- + +## 6. Debugging checklist + +1. **Run the input query alone** in the console builder — confirm the right-hand + results pane actually contains the data you reference. +2. **`$.` paths must match query aliases** — `item: function` → `$.item`, not `$.function`. +3. **Render preview** — the builder shows the rendered template *and* the final + schema-validated value. A red validation error means your output shape is wrong + (wrong keys, not JSON, stray whitespace). +4. **Test resource** — set a representative Test Resource; calc results depend on + *that* resource's CMDB data and its ancestors. +5. **`Skip` early** — if data may be absent (resource just discovered), branch to + `Skip` rather than emitting a broken value. +6. **Where you set it matters** — a setting on the resource affects only it; on an + account/folder it affects every matching descendant, each computing *its own* value. + +--- + +## 7. Further reading + +- [Calculated Policies 7-minute lab](https://turbot.com/guardrails/docs/getting-started/7-minute-labs/calc-policy) +- [Nunjucks templating](https://mozilla.github.io/nunjucks/templating.html) +- [Guardrails GraphQL API reference](https://turbot.com/guardrails/docs/reference/graphql) +- [Guardrails Filter language reference](https://turbot.com/guardrails/docs/reference/filter) +- [Mod schemas on the Hub](https://hub.guardrails.turbot.com/#mods) diff --git a/training/calc-policies/EXAMPLES.md b/training/calc-policies/EXAMPLES.md new file mode 100644 index 00000000..eaf51f24 --- /dev/null +++ b/training/calc-policies/EXAMPLES.md @@ -0,0 +1,222 @@ +# Calculated Policy Examples — Simple → Complex + +A teaching ladder of four worked calculated policies. Each shows the **GraphQL +input query**, the **Nunjucks template**, a sample **rendered output**, and the +**Terraform** equivalent. All four are drawn from real packs in this repo — the +file path is linked under each example so learners can open the source. + +> Read [`AUTHORING-GUIDE.md`](./AUTHORING-GUIDE.md) first for the concepts +> (`$.` paths, context pivots, `get(path:)`, output formats, whitespace). + +| # | Difficulty | Skill introduced | Policy family | +| - | ---------- | ---------------- | ------------- | +| 1 | 🟢 Simple | Read a value, format a map | Tags template | +| 2 | 🟢🟢 Simple+ | Conditionals on resource metadata | Tags template | +| 3 | 🟡 Medium | Loop + required-list check, `{title,result,message}` | Approved/Custom | +| 4 | 🔴 Complex | Aggregate over `descendants` | Approved/Custom | + +--- + +## 1 🟢 Static tag map (the "hello world") + +**Goal:** set a fixed set of tags via a template. Introduces the pipeline and +the YAML `key: value` output shape — no logic yet. + +**GraphQL input:** +```graphql +{ bucket { Name tags } } +``` + +**Template:** +```yaml +Company: "Vandelay Industries" +Department: "Sales" +Cost Center: "314159" +``` + +**Rendered value:** +```yaml +Company: "Vandelay Industries" +Department: "Sales" +Cost Center: "314159" +``` + +This is the literal starting point of the 7-minute lab. The next example adds +the conditional logic the lab builds toward. + +--- + +## 2 🟢🟢 Conditional tags from CMDB metadata + +**Goal:** stamp `creator` and `creationTime` tags pulled from the resource's +Guardrails metadata, only when present. Introduces `$.` paths, `if`, +`setAttribute`, `.split()`, and the empty-`[]` convention. + +📁 `policy_packs/aws/s3/enforce_creator_and_creationtime_tags_for_buckets/policies.tf` + +**GraphQL input:** +```graphql +{ + resource { + metadata + } +} +``` + +**Template:** +```nunjucks +{% set tags_plan = {} -%} + +{%- if $.resource.metadata.createdBy -%} + {%- set tags_plan = setAttribute(tags_plan, "creator", $.resource.metadata.createdBy) -%} +{%- endif -%} + +{%- if $.resource.metadata.createTimestamp -%} + {%- set tags_plan = setAttribute(tags_plan, "creationTime", $.resource.metadata.createTimestamp.split('T')[0]) -%} +{%- endif -%} + +{%- if tags_plan | length < 1 -%} + [] +{%- else -%} + {{ tags_plan | json }} +{%- endif -%} +``` + +**Rendered value:** +```json +{ "creator": "alice@example.com", "creationTime": "2026-01-14" } +``` + +**Why it matters:** the value adapts per resource. A freshly discovered bucket +with no metadata renders `[]` (set nothing) instead of breaking. + +--- + +## 3 🟡 Approved check over a required-tag list + +**Goal:** mark a resource Approved only if it carries every required tag. +Introduces the `{ title, result, message }` output contract for +`*ApprovedCustom` policies, plus looping over a configurable list. + +📁 `policy_packs/aws/lambda/enforce_functions_use_approved_tags/policies.tf` + +**GraphQL input:** +```graphql +{ + item: function { + tags: get(path: "Tags") + } +} +``` + +**Template:** +```nunjucks +{%- set tags = $.item.tags -%} +{%- set inputTagKeys = ["name", "environment"] -%} +{%- set tagsLength = tags | length -%} +{%- set allTagsPresent = true -%} +{%- set flag = true -%} + +{%- if tagsLength > 0 -%} + {%- for key in inputTagKeys -%} + {%- if flag and not key in tags -%} + {%- set allTagsPresent = false -%} + {%- set flag = false -%} + {%- endif -%} + {%- endfor -%} +{%- endif -%} + +{%- if tagsLength > 0 and allTagsPresent -%} + {%- set data = { + "title": "Approved Tags", + "result": "Approved", + "message": "Function has approved tags" + } -%} +{%- elif tagsLength == 0 or not allTagsPresent -%} + {%- set data = { + "title": "Approved Tags", + "result": "Not approved", + "message": "Function is missing one or more required tags" + } -%} +{%- endif %} + +{{ data | json }} +``` + +**Rendered value:** +```json +{ "title": "Approved Tags", "result": "Not approved", "message": "Function is missing one or more required tags" } +``` + +**Pair it:** this `functionApprovedCustom` template feeds the +`functionApproved` policy (`Check: Approved` / `Enforce: Delete unapproved if new`). +The `inputTagKeys` list is the one knob a customer customizes. + +--- + +## 4 🔴 Aggregate over descendants + +**Goal:** approve a VPC only if it has at least one Transit Gateway attachment. +Introduces relationship traversal (`descendants` with a filter) and aggregation +(`| length`) — the canonical "complex" calc policy. + +📁 `policy_packs/aws/vpc/enforce_vpcs_have_transit_gateways_attached/policies.tf` + +**GraphQL input:** +```graphql +{ + resource { + VpcId: get(path: "VpcId") + descendants(filter: "resourceTypeId:tmod:@turbot/aws-vpc-connect#/resource/types/transitGatewayAttachment level:self,descendant") { + items { + VpcId: get(path: "VpcId") + } + } + } +} +``` + +**Template:** +```nunjucks +{%- if $.resource.VpcId and $.resource.descendants.items | length == 0 %} + {% set data = { + "title": "Transit Gateway Attachment", + "result": "Not approved", + "message": "Transit Gateway is not attached to VPC" + } -%} +{%- elif $.resource.VpcId and $.resource.descendants.items | length > 0 %} + {% set data = { + "title": "Transit Gateway Attachment", + "result": "Approved", + "message": "Transit Gateway is attached to VPC" + } -%} +{%- else %} + {% set data = { + "title": "Transit Gateway Attachment", + "result": "Skip", + "message": "No data for VPC yet" + } -%} +{%- endif %} + +{{ data | json }} +``` + +**Rendered value:** +```json +{ "title": "Transit Gateway Attachment", "result": "Approved", "message": "Transit Gateway is attached to VPC" } +``` + +**What's new vs #3:** the decision depends on *related* resources, not the +resource's own attributes. The explicit `Skip` branch handles the window before +descendants are discovered. + +--- + +## Where to go next + +- Re-implement #2 for **Azure** (`virtualMachine` tags) or **GCP** to prove the + pattern is provider-agnostic. +- Take #4 and aggregate a **count threshold** (e.g. "≥ 2 subnets") instead of a + boolean presence check. +- Browse more live examples: any pack containing `template_input` in + `policy_packs/**/policies.tf`. diff --git a/training/calc-policies/README.md b/training/calc-policies/README.md new file mode 100644 index 00000000..03d23be9 --- /dev/null +++ b/training/calc-policies/README.md @@ -0,0 +1,34 @@ +# Calculated Policies — Training & Enablement + +Material for teaching developers and customers to write **calculated policies** +in Turbot Guardrails, from a one-value tag template up to descendant aggregation. + +## What's here + +| File | Use it for | +| ---- | ---------- | +| [`TRAINING-RUNBOOK.md`](./TRAINING-RUNBOOK.md) | A timed, 2-hour facilitator script for a hands-on customer training. | +| [`AUTHORING-GUIDE.md`](./AUTHORING-GUIDE.md) | A reference to keep open while writing calc policies (concepts, output contracts, debugging). | +| [`EXAMPLES.md`](./EXAMPLES.md) | A simple→complex ladder of four worked examples (GraphQL + Nunjucks + rendered output + Terraform). | + +## How they fit together + +``` +Concept ──▶ AUTHORING-GUIDE.md (the "how it works") +Practice ──▶ EXAMPLES.md (4 worked examples, simple → complex) +Deliver ──▶ TRAINING-RUNBOOK.md (2-hour session built on the 7-min lab) +``` + +- The hands-on console flow uses the **`calc-policy` 7-minute lab** in the + `guardrails-docs` repo. +- The production (Terraform) form lives in real packs under + `policy_packs/**/policies.tf` — see the file links inside `EXAMPLES.md`. +- The repo-wide `CLAUDE.md` has a **Calculated Policies** section so any Claude + Code session in this repo is fluent in the pattern. + +## Suggested path for a new author + +1. Skim `AUTHORING-GUIDE.md` §1–§3. +2. Reproduce Example #1 and #2 from `EXAMPLES.md` in the console builder. +3. Build an approval policy (Example #3), then an aggregation (Example #4). +4. Port your tested query + template into a `turbot_policy_setting` (`template_input` / `template`). diff --git a/training/calc-policies/TRAINING-RUNBOOK.md b/training/calc-policies/TRAINING-RUNBOOK.md new file mode 100644 index 00000000..d9b8edc5 --- /dev/null +++ b/training/calc-policies/TRAINING-RUNBOOK.md @@ -0,0 +1,164 @@ +# Calculated Policies — 2-Hour Training Runbook + +A facilitator script for delivering a hands-on calculated-policy training to a +customer. Built on top of the +[`calc-policy` 7-minute lab](https://turbot.com/guardrails/docs/getting-started/7-minute-labs/calc-policy) +and the worked examples in [`EXAMPLES.md`](./EXAMPLES.md). + +| | | +| --- | --- | +| **Audience** | Cloud / platform engineers who will author and ship policies | +| **Duration** | ~2 hours (120 min) including Q&A | +| **Format** | Live console demo + 2 hands-on exercises + Terraform bridge | +| **Outcome** | Attendees can build, test, and ship simple→complex calc policies | + +--- + +## Before the session — facilitator prep + +- [ ] Confirm each attendee can log into the Guardrails console as `Turbot/Admin` or `Turbot/Owner`. +- [ ] Confirm the `aws` and `aws-s3` mods are installed in the training workspace. +- [ ] Pre-create **one S3 bucket per attendee** (or a shared one) with a few tags, so there is a real Test Resource. +- [ ] Have `policy_packs/aws/.../policies.tf` examples open (see [`EXAMPLES.md`](./EXAMPLES.md)) for the Terraform segment. +- [ ] Share the [`AUTHORING-GUIDE.md`](./AUTHORING-GUIDE.md) link as the take-home reference. +- [ ] Decide: are attendees shipping via console only, or via Terraform too? (Drives how deep segment 6 goes.) + +--- + +## Agenda at a glance + +| Time | Segment | Mode | +| ---- | ------- | ---- | +| 0:00–0:15 | 1. Concept: why calculated policies | Talk | +| 0:15–0:35 | 2. Live demo: S3 Tags Template | Demo | +| 0:35–1:00 | 3. Exercise 1: build the tag template | Hands-on | +| 1:00–1:20 | 4. GraphQL deep-dive: pivots, get(), descendants | Talk + demo | +| 1:20–1:45 | 5. Exercise 2: conditional approval policy | Hands-on | +| 1:45–1:55 | 6. Ship it as Terraform | Demo | +| 1:55–2:00 | 7. Wrap-up & Q&A | Discussion | + +--- + +## Segment 1 — Concept (0:00–0:15) + +**Goal:** everyone understands *what* a calc policy is and *when* to reach for one. + +Talking points: +- Static value vs **calculated** value — "any policy in Guardrails can be calculated." +- The two-stage pipeline: **GraphQL input query → Nunjucks template → policy value.** +- Recomputes automatically when CMDB data changes. +- Where you set it matters: resource vs account/folder (each descendant computes *its own* value). + +Draw this on screen: +``` +CMDB ──(GraphQL)──▶ JSON ──(Nunjucks)──▶ policy value +``` + +**Checkpoint:** "Give me one policy in your environment that can't be a single static value." (Collect 1–2 answers — use them later.) + +--- + +## Segment 2 — Live demo (0:15–0:35) + +**Goal:** show the full console flow end-to-end before anyone touches it. + +Follow the 7-minute lab live for `AWS > S3 > Bucket > Tags > Template`: +1. **Policies → New Policy Setting**, select the policy type, pick a Test Resource bucket. +2. **Enable calculated mode → Launch calculated policy builder.** +3. Paste the **Step 2** query: `{ bucket { Name tags } }`. Point out the live results pane on the right. +4. Paste the **Step 3** Nunjucks template (the conditional tag example from the lab). +5. Walk the **rendered output** and **schema-validated value** panes at the bottom. +6. **Create**, then show the resulting control re-evaluate. + +Narrate: "Step 2 is *what data*, Step 3 is *what shape*. The right-hand panes are your feedback loop." + +--- + +## Segment 3 — Exercise 1 (0:35–1:00) + +**Goal:** muscle memory for the builder; output = a tags map. + +Attendees reproduce the demo on **their own** bucket, then extend it: +- Build the `bucketTagsTemplate` calc policy from scratch. +- Add a conditional: if `Environment` tag isn't one of `Dev/QA/Prod`, output `Non-Compliant Tag`. +- Confirm the rendered value changes when they change the Test Resource's tags. + +Facilitator circulates. Common stumbles to watch for: +- Referencing `$.bucket.tags` when the query aliased the node differently. +- Broken YAML from missing whitespace trims — point them to `{%- -%}`. + +**Checkpoint:** everyone has a green (schema-valid) rendered value. + +--- + +## Segment 4 — GraphQL deep-dive (1:00–1:20) + +**Goal:** unlock data beyond the resource's own attributes. + +Demo, building on the lab's "Expand your query" section: +- **Context pivots** — add `region { Name }`, `account { ... }`, `folder { ... }` and explain they pivot to ancestors automatically. +- **`get(path: "...")`** — add `grantee: get(path: "Acl.Grants[0].Grantee")` for attributes not in the schema. +- **`descendants(filter: ...)`** — preview the VPC/Transit-Gateway example (Example #4) to show aggregation. +- Show **where to find schemas**: Explore tab, Hub Inspect tab, builder auto-complete. + +**Checkpoint:** "Where would you look up the field name for X?" (Answer: Explore tab / Hub Inspect.) + +--- + +## Segment 5 — Exercise 2 (1:20–1:45) + +**Goal:** write a real *approval* policy with the `{title, result, message}` contract. + +Use **Example #3** (required-tag approval) as the template. Attendees: +- Pick an `*ApprovedCustom` policy type (Lambda function, or S3 bucket approved). +- Query the resource's tags with `get(path: "Tags")`. +- Loop over a required-tag list and emit `Approved` / `Not approved`. +- Add a `Skip` branch for "no data yet." + +Stretch goal (fast finishers): make the required-tag list driven by a higher-level +policy value instead of hard-coding it. + +**Checkpoint:** rendered output is a valid `{ "title", "result", "message" }` JSON object; control shows Approved/Not approved as expected. + +--- + +## Segment 6 — Ship it as Terraform (1:45–1:55) + +**Goal:** connect the console skill to how they'll actually deploy at scale. + +- Open `policy_packs/aws/lambda/enforce_functions_use_approved_tags/policies.tf`. +- Map the console builder to HCL: **Step 2 → `template_input`**, **Step 3 → `template`**. +- Show the pack pairs an **enforcement** policy with its **calculated input** policy. +- Mention the apply flow from the repo: `terraform init / plan / apply` with `TURBOT_WORKSPACE`, `TURBOT_ACCESS_KEY`, `TURBOT_SECRET_KEY`. + +Key message: "What you tested in the builder is copy-paste ready into a policy pack — same query, same template, version-controlled." + +--- + +## Segment 7 — Wrap-up & Q&A (1:55–2:00) + +- Recap the pipeline and the two output contracts (tags map vs `{title,result,message}`). +- Hand out references: [`AUTHORING-GUIDE.md`](./AUTHORING-GUIDE.md), [`EXAMPLES.md`](./EXAMPLES.md), the 7-minute lab, Nunjucks + GraphQL docs. +- Revisit the policies attendees named in Segment 1 — "which of those could you build now?" + +--- + +## Facilitator cheat-sheet + +| Symptom | Likely cause | Fix | +| ------- | ------------ | --- | +| Empty results pane | No/incomplete Test Resource, or wrong field name | Pick a real resource; check Explore tab for field names | +| `$.x is undefined` | `$.` path doesn't match query alias | Match alias exactly (`item: function` → `$.item`) | +| Red schema validation | Output shape wrong (keys / not JSON / whitespace) | Use `{{ data | json }}`; trim with `{%- -%}` | +| Control flaps / errors before discovery | No `Skip` branch | Add an `else → Skip` branch | +| Setting affects too many resources | Set too high in hierarchy | Set on the resource, not account/folder | + +--- + +## Take-home references + +- [`AUTHORING-GUIDE.md`](./AUTHORING-GUIDE.md) — concepts & debugging +- [`EXAMPLES.md`](./EXAMPLES.md) — simple→complex worked examples +- [Calculated Policies 7-minute lab](https://turbot.com/guardrails/docs/getting-started/7-minute-labs/calc-policy) +- [Nunjucks templating](https://mozilla.github.io/nunjucks/templating.html) +- [Guardrails GraphQL reference](https://turbot.com/guardrails/docs/reference/graphql)