Transform types: all 9 transformations used in GraphWarden rulesets
When a GraphWarden rule matches with action: filter, its response_filter can apply per-property transforms before the response returns to the caller. The nine transform types cover masking, redaction, hashing, truncation, regex substitution, domain extraction, initials, and noise injection. This page documents each transform with its YAML shape and a before/after example. See the rule schema reference for the top-level YAML contract and Condition types for the gating layer that decides when a transform runs.
Prerequisites
Transform mechanics
Transforms attach to properties inside a response_filter: block on a rule. The rule's action must be filter for transforms to run — any other action (allow, block, log_only) skips the response filter entirely. Each transform is a YAML object with a type field plus type-specific configuration fields, attached to the property whose value it should modify:
response_filter:
allow_properties:
- id
- displayName
- mail:
transform:
type: mask_end
visible: 5
mask_start
Hides the first N characters of a string with a mask character, preserving the suffix. Useful for partially revealing a value while hiding its start.
YAML shape
type: mask_start
visible: 5
mask_char: "*"
Before and after
| Input | Output |
|---|---|
"Alice Wonderland" |
"*********erland" |
"0123456789" |
"*****56789" |
Notes
visible is the count of trailing characters that remain readable (the tail length). mask_char defaults to * and may be any single character. When the input is shorter than visible, the entire string is returned unmasked. Empty strings and null values pass through untouched.
mask_end
Hides the last N characters of a string with a mask character, preserving the prefix. The mirror of mask_start.
YAML shape
type: mask_end
visible: 5
mask_char: "*"
Before and after
| Input | Output |
|---|---|
"alice@example.com" |
"alice************" |
"4111111111111111" |
"41111***********" |
Notes
visible is the count of leading characters that remain readable. Commonly used for partial-visibility display of email addresses, phone numbers, and account numbers. When the input is shorter than visible, the entire string is returned unmasked.
redact
Replaces the property value with a fixed sentinel string. Hides the value entirely without removing the property key from the response.
YAML shape
type: redact
replacement: "[redacted]"
Before and after
| Input | Output |
|---|---|
"alice@contoso.com" |
"[redacted]" |
"555-1234" |
"[redacted]" |
Notes
replacement defaults to "[redacted]" and may be any string. Prefer redact over strip_properties when the caller's client code expects the property key to exist in the response — redaction preserves the schema shape while hiding the value. For complete removal (key absent from the response), use strip_properties instead.
hash
Replaces the value with its SHA256 hex digest. Preserves uniqueness across responses while hiding the original value.
YAML shape
type: hash
algorithm: sha256
encoding: hex
Before and after
| Input | Output |
|---|---|
"alice@contoso.com" |
"4d5b2e1f..." (64-char SHA256 hex) |
"bob@contoso.com" |
"2a7f9c83..." (64-char SHA256 hex) |
Notes
algorithm currently accepts sha256 only; encoding accepts hex (default) or base64. Deterministic: the same input always yields the same output. Use when downstream consumers need a stable identifier for analytics or joins without the plaintext value. Because the algorithm is unsalted, the output is subject to rainbow-table inference for small value spaces — do not use hash as a substitute for encryption.
truncate
Trims a string to a configured maximum length. Values longer than the limit are clipped; values shorter are returned unchanged.
YAML shape
type: truncate
max_length: 50
suffix: "..."
Before and after
| Input | Output |
|---|---|
"This is a long description exceeding fifty characters here" |
"This is a long description exceeding fifty chara..." |
"Short text" |
"Short text" |
Notes
max_length counts characters of the truncated output including the suffix. suffix defaults to an empty string; set it to "..." to signal truncation explicitly. When max_length is less than the suffix length, the suffix is returned alone with no source characters. Byte-boundary handling treats multi-byte UTF-8 sequences as single characters — you will not split a code point.
regex_replace
Matches a PCRE pattern against the input and substitutes the match with a template string. Supports capture-group references in the replacement.
YAML shape
type: regex_replace
pattern: "(\\d{4})\\d{4}(\\d{4})"
replacement: "$1****$2"
Before and after
| Input | Output |
|---|---|
"411111112222 4444" (card-like) |
"4111****4444" (with pattern stripping spaces) |
"user@contoso.com" with pattern "^(.).*@" and replacement "$1***@" |
"u***@contoso.com" |
Notes
pattern is a PCRE regular expression. Backslashes inside YAML double-quoted strings must be escaped — prefer single-quoted YAML strings ('(\d{4})\d{4}(\d{4})') to keep the pattern readable. replacement supports $1, $2, etc. for capture-group backreferences. When the pattern does not match, the input is returned unchanged. Use anchored patterns (^...$) for predictable behaviour on sensitive fields; unanchored patterns can match unintended substrings.
domain_only
Extracts the domain portion of an email-shaped string. Drops the local part and the @ separator.
YAML shape
type: domain_only
Before and after
| Input | Output |
|---|---|
"alice@contoso.com" |
"contoso.com" |
"bob.smith@sub.fabrikam.com" |
"sub.fabrikam.com" |
Notes
The transform splits on the first @ character and returns the suffix. Inputs without an @ (for example, a display name) are returned unchanged — this is a silent pass-through rather than an error. Commonly paired with analytics pipelines that need domain-level aggregation without caller identity.
initials
Extracts initials from a display-name string. Takes the first character of each whitespace-separated token and uppercases them.
YAML shape
type: initials
max_length: 3
Before and after
| Input | Output |
|---|---|
"John Doe" |
"JD" |
"Alice May Wonderland" |
"AMW" |
"Jean-Luc Picard" (hyphenated) |
"JP" |
Notes
max_length caps the output length. Tokens are split on whitespace only; hyphenated or apostrophe-separated names produce a single initial per whitespace-separated token. Empty strings return empty. Diacritics on the first character are preserved ("Élise Durand" yields "ÉD").
noise
Appends a random suffix to a string, preserving the prefix. Breaks exact-match aggregation without fully redacting the value.
YAML shape
type: noise
suffix_length: 6
separator: "_"
Before and after
| Input | Output |
|---|---|
"alice" |
"alice_3f7k2b" |
"alice" (same input, second call) |
"alice_9m4p1q" |
Notes
The suffix is non-deterministic — the same input produces a different output on each call. Use noise to defeat downstream correlation (for example, to prevent log aggregators from grouping by user identifier) without losing the readable prefix. Not suitable when downstream consumers rely on value equality across responses; reach for hash for that case.
Transform ordering within a rule
When a property carries a transform, the transform runs after allow_properties whitelisting or strip_properties blacklisting and before the response is returned to the caller. A single property declares a single transform — chaining multiple transforms on the same property is not supported in a single rule. To chain (for example, regex_replace then truncate on the same field), split the logic across two rules whose match: blocks both cover the endpoint, ordered so the more-specific rule runs first under the ruleset's first-match-wins evaluation.
Within a single response filter, transforms on different properties run independently and order between them is not observable — each transform reads the original Graph response value for its property, not the output of any sibling transform.
Interaction with response filtering vs request transformation
Transforms operate on the outbound response from Graph, never on the inbound request. Request-side shaping (blocking, rate-limiting, caller-identity checks) is expressed through condition types on the rule, not through transforms. A rule with action: filter runs its transforms after Graph returns; a rule with action: block rejects the request before it reaches Graph and never runs transforms.
When response_filter declares allow_properties with a transform, the whitelist step runs first — properties absent from allow_properties are removed unconditionally, and only surviving properties with a transform: key run their transform. When response_filter declares strip_properties, the blacklist runs first and the transform runs on surviving properties. You can attach a transform to a property only when that property survives the whitelist or blacklist step.
Generate a transform for a GraphWarden response filter
Help me write a GraphWarden transform that {describe the input property, the privacy or shaping goal, and whether the output should be deterministic or random}. Use the 9 transform types documented at /docs/api-reference/transform-types, pick the single transform type that expresses the intent, and return a complete `response_filter` block with the property path and the transform configuration. Do not invent new transform type names; only the 9 documented types exist. If the intent needs multiple transforms on the same property, split across two rules ordered under the first-match-wins evaluation.
Reference: llms.txt
Troubleshooting
The transform is declared but the response is returned unchanged
The response filter is applied only when action: filter is set on the rule AND the rule's conditions evaluate true. If action is any other value (allow, block, log_only), the transform is skipped. Check the audit event for the request — a null rule_id means no rule matched and the default allow applied, not the filter-bearing rule. The What-If simulator in the Admin app shows which rule matched and which transforms ran.
regex_replace does not match the pattern I tested against in an online regex tool
Backslashes inside YAML double-quoted strings are interpreted by the YAML parser before reaching the regex engine. A pattern written "\d{4}" in double quotes reaches the regex engine as d{4}. Wrap regex patterns in single quotes ('\d{4}') to pass the backslashes through verbatim, or double-escape them ("\\d{4}"). Prefer single quotes — they are the least error-prone.
hash produces different outputs between two proxy instances for the same input
The hash transform is deterministic per algorithm and input — two instances running the same algorithm on the same input always produce the same hex digest. Differences indicate the input bytes differ across instances. Trailing whitespace, encoding differences (UTF-8 BOM), or case variations are the usual culprits; compare the raw bytes of the Graph response property across instances to isolate the divergence.
mask_start or mask_end returns the full string unmasked
When visible is greater than or equal to the input length, the transform returns the input unchanged — there is nothing left to mask. Confirm the expected input length; a short value like "ABC" with visible: 5 returns "ABC". For properties with highly variable length (free-text fields), prefer truncate plus a fixed suffix or regex_replace with an anchored pattern.
A transform on a nested property does not run
Transforms attach to property names inside allow_properties. Nested properties are reached through the response-shape nesting under the parent property — for example, manager.mail is a nested property under manager, not a top-level property. Declare the transform under the parent's nested list:
response_filter:
allow_properties:
- id
- displayName
- manager:
- id
- mail:
transform:
type: mask_end
visible: 5
For the gating layer that decides when each rule's transforms fire, see Condition types.
Last reviewed: