Rules Configuration
rules.json is the single configuration surface that controls what the agent does when a webhook arrives. Each entry in the JSON array is a self-contained rule set that maps a webhook name to one or more execution blocks. Each block independently declares payload filters, input extraction, plugin installation, and a Claude Code prompt template — so a single inbound event can fan out into multiple specialised workflows without any custom code.
If multiple execution blocks in the same rule set match one webhook payload, each match is scheduled as its own run (separate activation / executor session) with that block’s inputs, plugins, and prompt.
rules.json → WebhookRulesEvaluator → EventOrchestrator → ProcessingWorkflow → Executor ContainerIn the the-agent reference implementation, the default file is Knowledge/rules.json, embedded at agent registration as Xians knowledge document Rules.
File Structure
Section titled “File Structure”rules.json is a JSON array of rule set objects. Each rule set targets one webhook name (case-insensitive) and contains an executions array. Each execution is an independent pipeline: optional filters, inputs, plugins, and prompt.
[ { "webhook": "...", "executions": [ { "name": "...", "platform": "...", "repository": { "url": "...", "ref": "..." }, "match-any": [ ... ], "use-inputs": [ ... ], "use-plugins": [ ... ], "with-envs": [ ... ], "model": "...", "max-turns": 40, "allowed-tools": [ ... ], "disallowed-tools": [ ... ], "max-budget-usd": 1.00, "resume-sessions": false, "execute-prompt": "..." } ] }]| Field | Description |
|---|---|
webhook | Webhook name from Xians Agent Studio (must match incoming events) |
executions | One or more execution blocks; optional per-block name for logs and skip messages |
platform (per execution, optional) | Hosting service the run targets (github, azuredevops, …). Structural — describes where the run happens, independent of the plugin. See § 1b. |
repository (per execution, optional) | Structural binding for the repository being operated on. Each declared sub-field (url, ref) accepts either a JSON path (resolved against the payload) or a constant via { "value": "...", "constant": true }. Auto-resolved values are exposed to plugins as {{repository-url}} / {{repository-name}} / {{git-ref}}; {{repository-name}} is derived from url, never authored. Omit the whole block for executions that don’t operate on a repo. See § 1b. |
with-envs (per execution, optional) | Container env vars injected before the prompt runs. Each entry must declare its source explicitly: secrets.KEY (tenant Secret Vault), host.NAME (agent process env), or a literal with "constant": true. Bare names and unknown prefixes fail the activation. See § 5. |
model (per execution, optional) | Claude model this block runs on (e.g. claude-haiku-4-5, claude-sonnet-4-5). Omit to use the executor default. See § 7. |
max-turns (per execution, optional) | Hard cap on agent turns — the run aborts once this many tool-use round-trips complete. See § 7. |
allowed-tools (per execution, optional) | List of tool names auto-approved without a permission prompt. Does not restrict which tools are available; use disallowed-tools to block tools entirely. See § 7. |
disallowed-tools (per execution, optional) | List of tool names (or scoped patterns like "Bash(rm *)") to remove from the agent’s context. See § 7. |
max-budget-usd (per execution, optional) | Hard USD spend cap per run. The SDK aborts the run once this threshold is crossed. See § 7. |
resume-sessions (per execution, optional) | When true, back-to-back runs on the same conversation (same repo + PR/issue) resume the prior Claude Code session. Best-effort — a missing session falls back to a fresh run. See § 7. |
Each execution block that passes its match-any filters is scheduled independently when multiple blocks match the same payload.
Evaluation Flow
Section titled “Evaluation Flow”┌──────────────────────────────────────────────────────────────────────┐│ Incoming Webhook ││ name: "Default" payload: { "action": "opened", ... } │└───────────────────────────────┬──────────────────────────────────────┘ │ ┌───────────▼───────────┐ │ Find rule set where │ │ webhook matches │ └───────────┬───────────┘ │ ┌───────────▼───────────┐ │ For each execution: │ │ Evaluate match-any │──── No match? → skip block │ (OR across entries) │ └───────────┬───────────┘ │ At least one match-any passes ┌───────────▼───────────┐ │ Extract use-inputs │ │ from payload │ └───────────┬───────────┘ │ ┌───────────▼───────────┐ │ Interpolate │ │ execute-prompt │ │ with {{input-name}} │ └───────────┬───────────┘ │ ┌───────────▼───────────┐ │ Start executor with │ │ plugins + prompt │ └───────────────────────┘1. webhook
Section titled “1. webhook”Case-insensitive match against the webhook name configured in Xians Agent Studio.
"webhook": "Default"Only one rule set per webhook name is used — the first matching entry in the rules.json array wins.
1b. platform & repository — Structural Execution Context
Section titled “1b. platform & repository — Structural Execution Context”These two execution-level fields describe what the run operates on — independent of which plugin is used. They sit alongside match-any / use-inputs / use-plugins and are resolved before any plugin runs. The framework uses them directly (credential setup, workspace volume, worktree checkout, chat-side input resolution) and auto-injects the resolved values into XIANIX_INPUTS under canonical kebab-case keys, so plugin prompts and the executor entrypoint can read them off the same keys they always have.
"platform": "github","repository": { "url": "repository.clone_url", "ref": "pull_request.head.ref"}| Field | Type | Description |
|---|---|---|
platform | string literal | Hosting service (github, azuredevops, …). Used by the executor to pick the right git credential helper and is exposed to plugin prompts as {{platform}}. Empty / omitted means the executor will infer from the repo URL (defaults to github). |
repository.url | string (JSON path) or { value, constant } object | Either a JSON path that resolves to the clone URL (the common webhook-driven case) or a hard-coded literal via the constant form (see Hard-coding the repository). Mandatory when declared — if a declared JSON path doesn’t resolve, the execution block is skipped before any container starts. Exposed as {{repository-url}}. |
repository.ref | string (JSON path) or { value, constant } object | Either a JSON path that resolves to the git ref (branch, commit SHA, or tag), or a constant pinning the run to a fixed branch/tag. Mandatory when declared. Omit entirely to run against the bare-clone HEAD. Exposed as {{git-ref}} and used directly by Executor/entrypoint.sh to position the worktree before the prompt runs. |
{{repository-name}}is derived, not declared. A shortowner/repo-style identifier is computed from the resolvedrepository.url(platform-aware: GitHub, Azure DevOps_gitURLs, etc.) and auto-injected as{{repository-name}}. There is norepository.nameknob in the schema — clone URL and display name are kept in lockstep so they can never drift. If you need a different display name, pick a different clone URL.
Hard-coding the repository (constant form)
Section titled “Hard-coding the repository (constant form)”For runs whose repository or ref is fixed regardless of the webhook payload — cron pings, Slack triggers, single-tenant agents pinned to one repo, manual triggers — wrap the value in { "value": "...", "constant": true }:
"repository": { "url": { "value": "https://github.com/my-org/agent-target.git", "constant": true }, "ref": { "value": "main", "constant": true }}The bare-string shorthand ("url": "repository.clone_url") is just sugar for { "value": "repository.clone_url", "constant": false }, so existing rules need no changes. Mixed forms also fall out naturally — clone a fixed mirror but check out whatever ref the webhook says:
"repository": { "url": { "value": "https://github.com/my-org/mirror.git", "constant": true }, "ref": "pull_request.head.ref"}Constant URLs of course also drive {{repository-name}} — the derivation runs on the resolved URL regardless of how it was supplied.
Why are these separate from use-inputs?
Section titled “Why are these separate from use-inputs?”- They are structural — every webhook-triggered run on a repo needs them, regardless of plugin. Promoting them to execution-level removes per-plugin duplication and makes the contract explicit.
- The framework needs them before the plugin loop runs (clone target, credential helper, volume name, worktree ref) — they were already special-cased; now the schema reflects that.
repository.refis part of the binding (which repo, at which ref), not a free-form input the prompt happens to use — nesting it next tourlkeeps that relationship obvious.- The chat-driven path (
SupervisorSubagentTools.RunClaudeCodeOnRepository) treatsRepositoryUrl/RepositoryNameas first-class typed fields and derives the display name from the URL the same way the webhook path does. Aligning the webhook schema removes a subtle divergence. - Executions that don’t operate on a repo (e.g. Azure DevOps work-item analysis) just omit the
repositoryblock — no need formandatory: falseceremony on per-plugin inputs.
Wire-format
Section titled “Wire-format”Plugin prompts and Executor/entrypoint.sh always read structural values from these canonical XIANIX_INPUTS keys (platform, repository-url, repository-name, git-ref). The agent serialises the resolved structural values into the inputs dict under exactly these keys — they are not authored under use-inputs and the same key names are not used for anything else. repository-name is the derived value (from repository.url), not a separate path.
Mandatory semantics
Section titled “Mandatory semantics”The structural fields use the same skip-on-missing behaviour as a use-inputs entry with "mandatory": true:
- If a declared sub-field uses the JSON-path form (
"url": "repository.clone_url") and the path doesn’t resolve, the block is skipped with a clear error and no executor container starts. - The constant form (
{ "value": "...", "constant": true }) skips the resolution check entirely — the literal is taken verbatim, so a constant binding can’t fail mid-flight. An empty constant value ({ "value": "", "constant": true }) is treated as “field undeclared” rather than “field set to empty” — that’s an authoring mistake the framework refuses to silently propagate. - Other execution blocks in the same rule set are still evaluated — the failure is per-block.
platformis a literal so it always “resolves” — there’s nothing to fail.repository-nameis derived fromrepository.urland never fails on its own — if the URL is unparseable the raw URL flows through as the display name so logs stay useful.
2. match-any — Payload Filtering
Section titled “2. match-any — Payload Filtering”Inside each execution block, match-any is an array of filter rules evaluated with OR logic: the block passes if any entry matches. If match-any is omitted or empty, the block passes unconditionally.
"match-any": [ { "name": "pr-opened-event", "rule": "action==opened" }, { "name": "pr-synchronize-event", "rule": "action==synchronize" }]| Field | Description |
|---|---|
name | Human-readable label (for logging and skip reasons) |
rule | A filter expression — see syntax below |
Filter Expression Syntax
Section titled “Filter Expression Syntax”Each rule is a comparison of a JSON path against a literal value, optionally combined with && (AND) and || (OR) operators:
<json-path> <operator> <expected-value>Six operators are supported. All string comparisons are case-sensitive and ordinal.
| Operator | Meaning | Missing path returns |
|---|---|---|
== | Equals | false |
!= | Not equals | true |
^= | Starts with (string prefix match) | false |
!^= | Does not start with | true |
*= | Contains (substring match) | false |
!*= | Does not contain | true |
^=, !^=, *=, and !*= only match string values — they never match numbers, booleans, or null.
Two additional unary operators check whether a path exists (resolves to a non-null value) without comparing against a right-hand side:
| Operator | Meaning | Missing path returns |
|---|---|---|
? | Exists — path resolves and value is not null | false |
!? | Not exists — path is missing or value is null | true |
Unary operators are appended directly to the path with no value on the right:
// Passes when the payload has a non-null "pull_request.title""rule": "pull_request.title?"
// Passes when the payload does NOT have a "pull_request.draft" field (or it is null)"rule": "pull_request.draft!?"Compound Expressions
Section titled “Compound Expressions”Multiple conditions can be combined in a single rule using && (AND) and || (OR):
| Operator | Meaning | Precedence |
|---|---|---|
&& | AND — all conditions in the group must be true | Higher |
| ` | ` |
|| has lower precedence than &&. The rule is split into OR-groups first, then each group is split into AND-conditions.
// Both conditions must be true"rule": "eventType==workitem.updated&&status==Active"
// Either condition can be true"rule": "action==opened||action==reopened"
// Mixed: (A AND B) OR (C AND D)"rule": "eventType==created&&status==New||eventType==updated&&status==Active"Quoted Values
Section titled “Quoted Values”If the expected value contains && or || (or you want a single-quoted literal), wrap it in single quotes:
"rule": "assignee=='some-user <user@example.com>'"Quotes are optional for simple values. Both of these are equivalent:
"rule": "action==opened""rule": "action=='opened'"JSON Paths
Section titled “JSON Paths”JSON paths use dot notation to traverse the payload. Given:
{ "action": "opened", "pull_request": { "draft": false } }| Expression | Result |
|---|---|
action==opened | true |
action!=closed | true |
pull_request.draft==false | true |
action==closed | false |
Type coercion is handled automatically — strings, numbers, booleans, and null are compared against the literal on the right-hand side.
Property names that contain .
Section titled “Property names that contain .”If an object key contains a dot (common on Azure DevOps, e.g. System.AssignedTo), a plain dot-separated path would be ambiguous. Wrap that segment in double quotes so it is treated as a single property name:
resource.fields."System.AssignedTo".newValueresource.revision.fields."System.Title"Inside a double-quoted segment, a backslash escapes the next character (for example if the key itself needed a quote).
This applies to match rules and to use-inputs paths (see below).
Arrays: numeric indices
Section titled “Arrays: numeric indices”When the value at a path segment is a JSON array, a numeric segment selects the element at that index (zero-based):
items.0.idresource.reviewers.1.displayNameIf the index is out of range, the path does not resolve (== fails; != treats a missing path as not equal).
Arrays: wildcard * (match rules only)
Section titled “Arrays: wildcard * (match rules only)”For filter rules (match-any), a path segment * means “any element of the array at this point.” The prefix before * must resolve to an array. The suffix is evaluated against each element until one matches (for positive operators) or none match (for negative operators).
resource.reviewers.*.displayName=='xianix-agent'This passes if any reviewer object has displayName equal to xianix-agent. The wildcard works with all operators:
// passes if any label name starts with "hotfix/"labels.*.name^='hotfix/'
// passes if any message in the thread contains a keywordcomments.*.body*='needs review'
// passes if any reviewer has a non-null "email" fieldresource.reviewers.*.email?Only one * segment per path is supported.
Wildcard * is not supported in use-inputs paths — use a fixed numeric index there if you need a specific array element.
Operator Examples
Section titled “Operator Examples”Starts with (^= / !^=)
Match a branch that follows a naming convention, or filter events from a specific bot:
// Trigger only for feature branches"rule": "pull_request.head.ref^=feature/"
// Skip anything pushed by a bot account"rule": "sender.login!^=bot-"
// Azure DevOps: source branch is a release branch"rule": "resource.sourceRefName^=refs/heads/release/"Contains (*= / !*=)
Match free-form text fields like commit messages, PR titles, or notification messages:
// Trigger when a PR title signals a breaking change"rule": "pull_request.title*=BREAKING"
// Azure DevOps: react to specific activity messages"rule": "message.text*='updated the source branch'""rule": "message.text*='as a reviewer'"
// Skip draft descriptions that mention WIP"rule": "pull_request.body!*=[WIP]"Exists (? / !?)
Check whether a field is present (and non-null) in the payload — useful for optional fields that aren’t always sent:
// Only trigger when the payload carries a pull_request object"rule": "action==opened&&pull_request?"
// Trigger when a reviewer has been assigned (field present)"rule": "requested_reviewer.login?"
// Skip payloads that have no body text"rule": "pull_request.body?"
// Only match when the milestone is NOT set"rule": "pull_request.milestone!?"3. use-inputs — Payload Extraction
Section titled “3. use-inputs — Payload Extraction”Extracts values from the webhook payload into named variables. They are used for execute-prompt interpolation and are forwarded to the executor (for example as XIANIX_INPUTS).
Don’t put structural context here.
platform,repository-url,repository-name, andgit-refare declared at the execution level and auto-injected intoXIANIX_INPUTSfor you. Authoring them underuse-inputsis unsupported — the framework uses the structural fields for credential setup, volume management, worktree checkout, and chat-side input validation.
"use-inputs": [ { "name": "pr-number", "value": "number", "mandatory": true }, { "name": "pr-title", "value": "pull_request.title" }]| Field | Description |
|---|---|
name | Key in the extracted dictionary |
value | Dot-separated JSON path into the payload, or a literal when constant is true |
constant | (optional, default false) When true, value is used as-is instead of resolving a path |
mandatory | (optional, default false) When true, the execution block is skipped if this input resolves to null, an empty string, or a whitespace-only string |
When a mandatory input fails, the execution block is skipped with a clear error message listing which inputs were missing. Other execution blocks in the same rule set are still evaluated — a single missing mandatory input does not abort the entire webhook.
Path Resolution Examples
Section titled “Path Resolution Examples”Given:
{ "number": 42, "repository": { "clone_url": "https://github.com/acme/app.git", "full_name": "acme/app" }, "pull_request": { "title": "Fix auth bug", "head": { "ref": "fix/auth" } }}| Input definition | Resolved value |
|---|---|
"value": "number" | 42 |
"value": "pull_request.head.ref" | "fix/auth" |
"value": "pull_request.title" | "Fix auth bug" |
"value": "high", "constant": true | "high" (literal) |
"value": "resource.revision.fields.\"System.Title\"" (path uses a quoted segment for a dotted key) | Azure DevOps work item System.Title |
Need the clone URL, repo name, platform, or checked-out ref in your prompt? Reference
{{repository-url}},{{repository-name}},{{platform}}, or{{git-ref}}directly — they’re auto-injected from the structural fields.
If a path does not resolve (missing property), the input is set to null. If the input is marked "mandatory": true, the entire execution block is skipped instead.
4. use-plugins — Plugin Installation
Section titled “4. use-plugins — Plugin Installation”Declares Claude Code marketplace plugins to install in the executor container before the prompt runs.
"use-plugins": [ { "plugin-name": "pr-reviewer@xianix-plugins-official", "marketplace": "xianix-team/plugins-official" }]| Field | Required | Description |
|---|---|---|
plugin-name | Yes | Plugin reference in plugin-name@marketplace-name form, passed to claude plugin install |
marketplace | No | Marketplace source (owner/repo, git URL, path, or marketplace.json URL). Omit for the built-in Anthropic marketplace. |
Heads-up — credentials a plugin needs (GitHub PAT, Azure DevOps PAT, third-party API keys) are not declared per-plugin. They live at the execution-block level in
with-envsso a single value likeGITHUB-TOKENonly has to be written once even when multiple plugins consume it.
5. with-envs — Container Environment Variables
Section titled “5. with-envs — Container Environment Variables”Declares environment variables to inject into the executor container before the prompt runs. Sits at the execution-block level (sibling to use-plugins) — every variable is available to every plugin and to the prompt itself, regardless of how many plugins consume it.
"with-envs": [ { "name": "GITHUB-TOKEN", "value": "secrets.GITHUB-TOKEN", "mandatory": true }, { "name": "REVIEW_MODE", "value": "strict", "constant": true }]The executor container already has a small set of agent-managed variables present before any plugin runs. with-envs lets you add to that set — for tenant credentials, plugin configuration flags, or any value the prompt or its plugins need.
Variables automatically present in the container
Section titled “Variables automatically present in the container”The only variable seeded into every container from the agent host is:
| Variable | Description |
|---|---|
ANTHROPIC_API_KEY | Anthropic API key (read directly by the Claude Code SDK). Set via ANTHROPIC-API-KEY in the agent’s .env — same value for every tenant. |
CM platform tokens (GITHUB-TOKEN, AZURE-DEVOPS-TOKEN, …) are not read from the agent host. Each tenant must store their own in the Xians Secret Vault and declare them in rules.json via with-envs:
"with-envs": [ { "name": "GITHUB-TOKEN", "value": "secrets.GITHUB-TOKEN", "mandatory": true }, { "name": "AZURE-DEVOPS-TOKEN", "value": "secrets.AZURE-DEVOPS-TOKEN", "mandatory": true }]This guarantees that two tenants never share the same platform credential — a tenant whose vault is missing the secret fails fast (when paired with mandatory: true) instead of silently borrowing a host-wide token.
Renaming a value for a plugin
Section titled “Renaming a value for a plugin”Some Claude Code plugins expect a specific variable name that differs from the credential’s canonical name. Use with-envs to expose the value under the name the plugin requires — the lookup form (secrets.*, host.*, or constant) determines where the value comes from, while name controls how the container sees it:
{ "name": "GITHUB_PERSONAL_ACCESS_TOKEN", "value": "secrets.GITHUB-TOKEN" }This fetches GITHUB-TOKEN from the tenant Secret Vault and makes it available as GITHUB_PERSONAL_ACCESS_TOKEN inside the container — so the plugin can find it without any changes to how the credential is stored.
Three value forms at a glance
Section titled “Three value forms at a glance”The value field supports three resolution forms — every entry must pick one explicitly. Bare names and unrecognised prefixes (including the legacy env.X) fail the activation with a non-retryable error so a typo can never silently leak a host env var into the container:
| Form | Resolved from | When to use |
|---|---|---|
host.VAR_NAME | Agent process environment (.env file / host env vars) | Genuinely host-wide settings that are the same for every tenant (e.g. ANTHROPIC-API-KEY, deployment knobs) |
secrets.SECRET-KEY | Tenant-scoped Xians Secret Vault (encrypted at rest) | Per-tenant credentials — GitHub PAT, Azure DevOps PAT, third-party API keys. The recommended (and only) place for credentials that differ per tenant. |
Literal + "constant": true | The string is used verbatim | Plugin flags, region identifiers, public URLs, anything that isn’t a credential |
host. reference syntax
Section titled “host. reference syntax”Prefix the value with host. to read a variable from the agent host (the agent process environment, populated from the agent’s .env file or whatever the deployment exports). The host. prefix is stripped and the remainder is the variable name to look up:
{ "name": "MY_PLUGIN_TOKEN", "value": "host.GITHUB_TOKEN" }{ "name": "AZURE_PAT", "value": "host.AZURE_DEVOPS_TOKEN" }{ "name": "CUSTOM_SERVICE_KEY", "value": "host.MY_CUSTOM_API_KEY" }If the referenced variable is not set on the host, the injected value will be an empty string. Combine with "mandatory": true to fail-fast instead.
Use
host.*sparingly. Anything tenant-specific belongs in the Secret Vault (secrets.*) —host.*is for values that are genuinely the same for every tenant on the agent.
secrets. reference syntax
Section titled “secrets. reference syntax”Prefix the value with secrets. to fetch the credential from the tenant-scoped Xians Secret Vault at container-start time. The secrets. prefix is stripped and the remainder is treated as the secret key to look up in the active tenant’s vault:
{ "name": "GITHUB-TOKEN", "value": "secrets.GITHUB-TOKEN", "mandatory": true }{ "name": "OPENAI_API_KEY", "value": "secrets.openai-api-key", "mandatory": true }{ "name": "STRIPE_WEBHOOK_SECRET", "value": "secrets.stripe-webhook-secret" }Under the hood, the agent runs the equivalent of:
var vault = XiansContext.CurrentAgent.Secrets.TenantScope();var fetched = await vault.FetchByKeyAsync("GITHUB-TOKEN");// fetched.Value is injected as the named env var inside the container.Resolution rules:
- Tenant scope is automatic. The lookup is bound to the tenant that owns the inbound webhook — different tenants can store different values under the same key without colliding.
- Encrypted at rest. Values are stored AES-256-GCM-encrypted server-side; the agent only ever sees the decrypted plaintext in memory while building the container env.
- No host-level fallback for platform credentials. The agent host’s
.envno longer providesGITHUB-TOKEN/AZURE-DEVOPS-TOKEN— these must live in each tenant’s vault, so a misconfigured tenant can never silently borrow another tenant’s PAT. - Missing or empty secret → the value resolves to an empty string. Combine with
"mandatory": true(see below) to fail-fast instead of starting the container with a blank credential. - Vault errors are non-fatal unless the entry is also
mandatory— they are logged and the resolved value is empty. - Rotation is hot. Updating a secret in the vault takes effect on the next container start; no agent restart or redeploy is required.
Manage the underlying secrets through the Xians Secret Vault (Agent API at api/agent/secrets, or any UI/CLI built on top of it) — supports create, list, update, and delete with strict per-tenant scope enforcement.
Constant values
Section titled “Constant values”Set "constant": true to inject a fixed literal string rather than resolving a host variable or a vault secret. This is useful for plugin configuration flags, region identifiers, or any value that does not come from the environment:
{ "name": "REVIEW_MODE", "value": "strict", "constant": true }{ "name": "TARGET_BRANCH", "value": "main", "constant": true }{ "name": "AZURE_ORG_URL", "value": "https://dev.azure.com/my-org", "constant": true }Mandatory entries
Section titled “Mandatory entries”Set "mandatory": true to make the executor container fail to start (non-retryably) when the resolved value is null or empty. This is the recommended pattern for any secret the prompt cannot run without:
{ "name": "GITHUB-TOKEN", "value": "secrets.GITHUB-TOKEN", "mandatory": true }The error message lists which env vars were missing and where to set them — the tenant Secret Vault for secrets.* entries, or the agent host .env for host.* entries.
Field reference
Section titled “Field reference”| Field | Description |
|---|---|
name | Name of the environment variable as it will appear inside the container |
value | Must use one of three explicit forms: host.VAR_NAME (read from the agent host environment), secrets.SECRET-KEY (read from the tenant Secret Vault), or a literal string when constant is true. Bare names and unrecognised prefixes (including the legacy env.X) fail the activation with a non-retryable error. |
constant | (optional, default false) When true, value is used as-is without any host or vault lookup |
mandatory | (optional, default false) When true, the executor container fails to start (non-retryable) if the resolved value is null or empty |
6. execute-prompt — Claude Code Prompt Template
Section titled “6. execute-prompt — Claude Code Prompt Template”A string template run as the Claude Code prompt after plugins are installed. Use {{input-name}} placeholders for resolved use-inputs values.
"execute-prompt": "You are reviewing PR #{{pr-number}} titled \"{{pr-title}}\" in {{repository-name}} (branch: {{git-ref}}).\n\nRun /code-review to perform the automated review."Placeholders are replaced case-insensitively. Any {{name}} with no matching input is left unchanged.
7. Cost & Execution Controls
Section titled “7. Cost & Execution Controls”Six optional fields on every execution block let you tune cost, speed, and safety — from picking a cheaper model to hard-capping spend. All are omitted by default so existing rules work unchanged.
{ "model": "claude-haiku-4-5", "max-turns": 40, "allowed-tools": ["Read", "Grep", "Bash"], "disallowed-tools": ["WebSearch", "WebFetch"], "max-budget-usd": 1.00, "resume-sessions": true}model — Model selection
Section titled “model — Model selection”Route a block to a specific Claude model. Omit to use the executor’s configured default (Sonnet-class).
"model": "claude-haiku-4-5"Use a cheaper model for mechanical tasks (requirement analysis, simple summaries) and the full Sonnet for deep reasoning (PR reviews, architecture decisions):
{ "name": "github-issue-triage", "model": "claude-haiku-4-5", ... }{ "name": "github-pull-request-review", "model": "claude-sonnet-4-5", ... }Regardless of the main model, Claude Code’s internal background work (session titles, mini-summaries) is always routed to a Haiku-class model by the executor — you don’t need to configure that separately.
max-turns — Turn cap
Section titled “max-turns — Turn cap”Limits the number of tool-use round-trips the agent is allowed. Once the cap is reached the run completes with whatever the agent produced up to that point.
"max-turns": 40The container wall-clock timeout (CONTAINER-EXECUTION-TIMEOUT-SECONDS) is always the final backstop. max-turns is a token backstop that fires before the clock runs out, preventing runaway loops on complex repos.
A host-wide opt-in default can be set via EXECUTOR-DEFAULT-MAX-TURNS in the agent .env — this applies to every run that doesn’t set its own max-turns.
allowed-tools and disallowed-tools — Tool control
Section titled “allowed-tools and disallowed-tools — Tool control”These two fields work differently and serve different purposes:
| Field | Effect |
|---|---|
allowed-tools | Auto-approves the listed tools (no permission prompt). Unlisted tools are still available and fall through to the executor’s bypassPermissions mode. |
disallowed-tools | Removes the listed tools from the agent’s context entirely. The agent cannot see or use them. |
Restriction requires
disallowed-tools. Because the executor runs inbypassPermissionsmode,allowed-toolsalone does not restrict anything — every tool is already approved. To actually block a tool, add it todisallowed-tools.
"disallowed-tools": ["WebSearch", "WebFetch"]You can also scope a denial to a pattern within a tool rather than blocking it entirely. A bare name blocks the whole tool; a scoped form like "Bash(rm *)" only blocks calls that match the pattern:
"disallowed-tools": ["WebSearch", "Bash(rm *)"]max-budget-usd — Spend cap
Section titled “max-budget-usd — Spend cap”Hard USD cap per run, passed to the Claude Code SDK. The run is aborted by the SDK once cumulative token spend crosses this threshold.
"max-budget-usd": 1.50The configured budget and an over-budget flag are recorded in metrics, so you can chart how often a block is hitting its cap and tune accordingly.
resume-sessions — Session reuse
Section titled “resume-sessions — Session reuse”When true, the executor persists the Claude Code session ID on the tenant volume after each run and resumes it on the next run against the same conversation (same repo + PR or issue number). This means the agent remembers what it read and did on the previous review instead of rediscovering the codebase from scratch.
"resume-sessions": trueThis is most valuable for bursty flows — a PR that receives multiple pushes in quick succession, or an issue that gets re-analysed after a comment. On the first run (or when no prior session is found) a fresh session starts automatically, so the flag is always safe to set.
Best-effort. A missing or expired session silently falls back to a fresh run — the flag never causes a failure.
Cached repository context (CLAUDE.md + symbol map)
Section titled “Cached repository context (CLAUDE.md + symbol map)”Independent of the per-block fields above, the executor prepares a cached orientation for every repo so the agent doesn’t re-explore the codebase cold on every run — the single biggest avoidable token sink. Two artifacts are built deterministically (no LLM cost) from the checked-out code and cached on the tenant volume, keyed by the branch HEAD (so they’re rebuilt only when HEAD moves):
CLAUDE.md— project overview, detected stack/commands, top-level layout, and a pointer to the symbol map. Claude Code auto-loadsCLAUDE.mdfrom the working directory..xianix/repomap.txt— a compact file→symbol map (functions/classes per file) so the agent can locate code by symbol instead of grepping.
Your
CLAUDE.mdalways wins. If your repository already ships aCLAUDE.md, the executor leaves it completely untouched — nothing is overwritten or appended — and the optional LLM pass below is skipped.
Optional hybrid narrative (host opt-in). When the operator enables it (host EXECUTOR-CONTEXT-LLM=1, or per rule-set via an XIANIX-CONTEXT-LLM entry in with-envs), a cheap, turn- and time-capped Haiku pass appends an Architecture & conventions narrative to the generated CLAUDE.md — the why the deterministic facts can’t capture. It runs at most once per HEAD change (on a cache miss), so its cost is amortised across every later run that reuses the cache, and any failure silently falls back to the deterministic-only CLAUDE.md.
Field reference
Section titled “Field reference”| Field | Type | Default | Description |
|---|---|---|---|
model | string | (executor default) | Claude model name (e.g. claude-haiku-4-5, claude-sonnet-4-5). |
max-turns | integer | (none) | Maximum agent turns before the run completes. |
allowed-tools | string array | [] | Tools to auto-approve (does not restrict). |
disallowed-tools | string array | [] | Tools to remove from the agent’s context. Accepts bare names ("WebSearch") or scoped patterns ("Bash(rm *)"). |
max-budget-usd | number | (none) | USD spend cap; run aborted by the SDK once crossed. |
resume-sessions | boolean | false | Resume the prior session for this conversation (repo + PR/issue). |
Complete Example
Section titled “Complete Example”A rule set with two executions — a Sonnet-powered PR review with a spend cap and session reuse, plus a Haiku-powered issue analysis with a turn cap:
[ { "webhook": "Default", "with-envs": [ { "name": "GITHUB-TOKEN", "value": "secrets.GITHUB-TOKEN", "mandatory": true } ], "executions": [ { "name": "github-pull-request-review", "platform": "github", "repository": { "url": "repository.clone_url", "ref": "pull_request.head.ref" }, "match-any": [ { "name": "pr-opened", "rule": "action==opened" }, { "name": "pr-synchronize", "rule": "action==synchronize&&pull_request.labels.*.name=='ai-dlc/pr/pr-review'" } ], "use-inputs": [ { "name": "pr-number", "value": "number", "mandatory": true }, { "name": "pr-title", "value": "pull_request.title" } ], "use-plugins": [ { "plugin-name": "pr-reviewer@xianix-plugins-official", "marketplace": "xianix-team/plugins-official" } ], "model": "claude-sonnet-4-5", "max-turns": 60, "disallowed-tools": ["WebSearch", "WebFetch"], "max-budget-usd": 2.50, "resume-sessions": true, "execute-prompt": "You are reviewing pull request #{{pr-number}} titled \"{{pr-title}}\" in the repository {{repository-name}} (branch: {{git-ref}}).\n\nRun /pr-review to perform the automated review. The `gh` CLI is authenticated and available if you need it directly." }, { "name": "github-issue-requirement-analysis", "platform": "github", "repository": { "url": "repository.clone_url" }, "match-any": [ { "name": "issue-labeled", "rule": "action==labeled&&label.name=='ai-dlc/issue/analyze'" } ], "use-inputs": [ { "name": "issue-number", "value": "issue.number", "mandatory": true } ], "use-plugins": [ { "plugin-name": "req-analyst@xianix-plugins-official", "marketplace": "xianix-team/plugins-official" } ], "model": "claude-haiku-4-5", "max-turns": 30, "execute-prompt": "Issue #{{issue-number}} in {{repository-name}} has been assigned for requirement analysis.\n\nRun /requirement-analysis {{issue-number}} to perform the automated analysis." } ] }]Work-item example (no repository)
Section titled “Work-item example (no repository)”Executions that don’t operate on a repo simply omit the repository block. platform can still be set to drive credential resolution:
[ { "webhook": "Default", "executions": [ { "name": "azuredevops-workitem-triage", "platform": "azuredevops", "match-any": [ { "name": "workitem-created", "rule": "eventType==workitem.created" } ], "use-inputs": [ { "name": "workitem-id", "value": "resource.id", "mandatory": true }, { "name": "workitem-title", "value": "resource.fields.\"System.Title\"" } ], "use-plugins": [ { "plugin-name": "workitem-triage@xianix-plugins-official", "marketplace": "xianix-team/plugins-official" } ], "with-envs": [ { "name": "AZURE-DEVOPS-TOKEN", "value": "secrets.AZURE-DEVOPS-TOKEN", "mandatory": true } ], "execute-prompt": "Triage Azure DevOps work item #{{workitem-id}}: \"{{workitem-title}}\". Run /workitem-triage to suggest area path, iteration, and labels." } ] }]Azure DevOps example: work item field with a dotted name
Section titled “Azure DevOps example: work item field with a dotted name”Filter when a field whose key contains dots changes (quoted segment):
"rule": "eventType==workitem.updated&&resource.fields.\"System.AssignedTo\".newValue=='xianix-agent <xianix-agent@99x.io>'"Azure DevOps example: PR updated with a specific reviewer
Section titled “Azure DevOps example: PR updated with a specific reviewer”Require both the event type and the agent in the reviewers list:
"rule": "eventType==git.pullrequest.updated&&resource.reviewers.*.displayName=='xianix-agent'"Azure DevOps example: PR activity via contains
Section titled “Azure DevOps example: PR activity via contains”Trigger on specific activity messages such as a source branch push or a reviewer being assigned:
// Source branch was updated"rule": "eventType==git.pullrequest.updated&&resource.reviewers.*.displayName=='xianix-agent'&&message.text*='updated the source branch'"
// Agent was added as a reviewer"rule": "eventType==git.pullrequest.updated&&resource.reviewers.*.displayName=='xianix-agent'&&message.text*='as a reviewer'"Example: branch-convention filter with starts-with
Section titled “Example: branch-convention filter with starts-with”Only run the workflow for pull requests targeting a release/ branch:
"rule": "action==opened&&pull_request.base.ref^=release/"What Happens at Runtime
Section titled “What Happens at Runtime”- A webhook fires with a name that matches
webhook(e.g."Default"). - For each execution block, if
match-anyis non-empty, at least onerulemust pass. - Structural fields are resolved first —
platform,repository.url,repository.ref. JSON-path bindings are looked up against the payload; constant bindings ({ "value": "...", "constant": true }) are taken verbatim. If a declared path doesn’t resolve, the block is skipped — constants never fail to resolve. use-inputsare resolved from the payload, and the resolved structural values are auto-injected back into the inputs dict under the canonical keysplatform/repository-url/git-ref. The shortrepository-name(e.g.owner/repo) is derived fromrepository-url(platform-aware: handles GitHub, Azure DevOps_gitURLs, etc.) and injected alongside them so prompts and plugins see a single combined view.execute-promptis interpolated with those inputs (including the auto-injected structural values).- The agent resolves
with-envs(literals,host.*,secrets.*) and injects them into the executor container alongside the runtime values it manages itself (ANTHROPIC_API_KEY, etc.). - Cost & execution controls are applied —
model,max-turns,allowed-tools,disallowed-tools,max-budget-usd, andresume-sessionsare forwarded to the executor as typed env vars (XIANIX-MODEL,XIANIX-MAX-TURNS, etc.). Any field that is not set is simply not seeded, so the executor falls back to its own defaults — there is no behavioral change for existing rules that don’t declare these fields. - The executor installs
use-plugins, injects a cachedCLAUDE.mdand symbol map into the worktree so the agent doesn’t re-explore the codebase from scratch (skipped when the repo ships its ownCLAUDE.md; optionally enriched with an LLM-authored architecture narrative whenEXECUTOR-CONTEXT-LLM/XIANIX-CONTEXT-LLMis enabled), optionally resumes a prior session (whenresume-sessions: true), and runs the final prompt with the configured model, turn cap, tool restrictions, and spend cap applied.