Skip to content

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 Container

In the the-agent reference implementation, the default file is Knowledge/rules.json, embedded at agent registration as Xians knowledge document Rules.


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": "..."
}
]
}
]
FieldDescription
webhookWebhook name from Xians Agent Studio (must match incoming events)
executionsOne 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.

┌──────────────────────────────────────────────────────────────────────┐
│ 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 │
└───────────────────────┘

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"
}
FieldTypeDescription
platformstring literalHosting 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.urlstring (JSON path) or { value, constant } objectEither 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.refstring (JSON path) or { value, constant } objectEither 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 short owner/repo-style identifier is computed from the resolved repository.url (platform-aware: GitHub, Azure DevOps _git URLs, etc.) and auto-injected as {{repository-name}}. There is no repository.name knob 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.

  • 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.ref is part of the binding (which repo, at which ref), not a free-form input the prompt happens to use — nesting it next to url keeps that relationship obvious.
  • The chat-driven path (SupervisorSubagentTools.RunClaudeCodeOnRepository) treats RepositoryUrl / RepositoryName as 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 repository block — no need for mandatory: false ceremony on per-plugin inputs.

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.

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.
  • platform is a literal so it always “resolves” — there’s nothing to fail.
  • repository-name is derived from repository.url and never fails on its own — if the URL is unparseable the raw URL flows through as the display name so logs stay useful.

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" }
]
FieldDescription
nameHuman-readable label (for logging and skip reasons)
ruleA filter expression — see syntax below

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.

OperatorMeaningMissing path returns
==Equalsfalse
!=Not equalstrue
^=Starts with (string prefix match)false
!^=Does not start withtrue
*=Contains (substring match)false
!*=Does not containtrue

^=, !^=, *=, 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:

OperatorMeaningMissing path returns
?Exists — path resolves and value is not nullfalse
!?Not exists — path is missing or value is nulltrue

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!?"

Multiple conditions can be combined in a single rule using && (AND) and || (OR):

OperatorMeaningPrecedence
&&AND — all conditions in the group must be trueHigher
``

|| 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"

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 use dot notation to traverse the payload. Given:

{ "action": "opened", "pull_request": { "draft": false } }
ExpressionResult
action==openedtrue
action!=closedtrue
pull_request.draft==falsetrue
action==closedfalse

Type coercion is handled automatically — strings, numbers, booleans, and null are compared against the literal on the right-hand side.

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".newValue
resource.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).

When the value at a path segment is a JSON array, a numeric segment selects the element at that index (zero-based):

items.0.id
resource.reviewers.1.displayName

If the index is out of range, the path does not resolve (== fails; != treats a missing path as not equal).

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 keyword
comments.*.body*='needs review'
// passes if any reviewer has a non-null "email" field
resource.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.

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!?"

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, and git-ref are declared at the execution level and auto-injected into XIANIX_INPUTS for you. Authoring them under use-inputs is 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" }
]
FieldDescription
nameKey in the extracted dictionary
valueDot-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.

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 definitionResolved 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.


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"
}
]
FieldRequiredDescription
plugin-nameYesPlugin reference in plugin-name@marketplace-name form, passed to claude plugin install
marketplaceNoMarketplace 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-envs so a single value like GITHUB-TOKEN only 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:

VariableDescription
ANTHROPIC_API_KEYAnthropic 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.

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.

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:

FormResolved fromWhen to use
host.VAR_NAMEAgent 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-KEYTenant-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": trueThe string is used verbatimPlugin flags, region identifiers, public URLs, anything that isn’t a credential

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.

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 .env no longer provides GITHUB-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.

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 }

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.

FieldDescription
nameName of the environment variable as it will appear inside the container
valueMust 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.


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
}

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.

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": 40

The 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:

FieldEffect
allowed-toolsAuto-approves the listed tools (no permission prompt). Unlisted tools are still available and fall through to the executor’s bypassPermissions mode.
disallowed-toolsRemoves the listed tools from the agent’s context entirely. The agent cannot see or use them.

Restriction requires disallowed-tools. Because the executor runs in bypassPermissions mode, allowed-tools alone does not restrict anything — every tool is already approved. To actually block a tool, add it to disallowed-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 *)"]

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.50

The 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.

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": true

This 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-loads CLAUDE.md from 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.md always wins. If your repository already ships a CLAUDE.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.

FieldTypeDefaultDescription
modelstring(executor default)Claude model name (e.g. claude-haiku-4-5, claude-sonnet-4-5).
max-turnsinteger(none)Maximum agent turns before the run completes.
allowed-toolsstring array[]Tools to auto-approve (does not restrict).
disallowed-toolsstring array[]Tools to remove from the agent’s context. Accepts bare names ("WebSearch") or scoped patterns ("Bash(rm *)").
max-budget-usdnumber(none)USD spend cap; run aborted by the SDK once crossed.
resume-sessionsbooleanfalseResume the prior session for this conversation (repo + PR/issue).

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."
}
]
}
]

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/"
  1. A webhook fires with a name that matches webhook (e.g. "Default").
  2. For each execution block, if match-any is non-empty, at least one rule must pass.
  3. Structural fields are resolved firstplatform, 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.
  4. use-inputs are resolved from the payload, and the resolved structural values are auto-injected back into the inputs dict under the canonical keys platform / repository-url / git-ref. The short repository-name (e.g. owner/repo) is derived from repository-url (platform-aware: handles GitHub, Azure DevOps _git URLs, etc.) and injected alongside them so prompts and plugins see a single combined view.
  5. execute-prompt is interpolated with those inputs (including the auto-injected structural values).
  6. 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.).
  7. Cost & execution controls are appliedmodel, max-turns, allowed-tools, disallowed-tools, max-budget-usd, and resume-sessions are 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.
  8. The executor installs use-plugins, injects a cached CLAUDE.md and symbol map into the worktree so the agent doesn’t re-explore the codebase from scratch (skipped when the repo ships its own CLAUDE.md; optionally enriched with an LLM-authored architecture narrative when EXECUTOR-CONTEXT-LLM / XIANIX-CONTEXT-LLM is enabled), optionally resumes a prior session (when resume-sessions: true), and runs the final prompt with the configured model, turn cap, tool restrictions, and spend cap applied.