File format
Every top-level field, what it does, and what the loader rejects.
Skeleton
$schema: https://scm.kebab.sh/strat/config.json # optional, for editors
name: storm-hitter # strongly recommended
description: Storm hitter that buffs then hits. # optional
school: storm # optional metadata
clients: [p2] # optional scope
enchant: { ... } # optional
targets: { ... } # optional
fallback: pass # optional
retry_failed_casts: true # optional
cast_retry_limit: 2 # optional
phases: # required
main:
priority: 100
when: { ... }
actions: [ ... ]
$schema
Optional URI for editor validation. Explicitly permitted by the loader, so it does not trip the unknown-field check.
name
Optional in the file, but always set it. When omitted the loader derives the name from the
filename minus its extension — so storm_cp.yml becomes storm_cp. That works until someone
renames the file and every reference to it breaks silently.
The name is what a plugin passes as strategy to automation:EnsureCombat, what the desktop
app's trainer lists it under wherever a strategy is selectable, and what you type in the
console's enable combat <name>.
Both hosts store loaded strategies in a table keyed by name:. Nothing warns you about a
collision — the second file to load simply replaces the first, and the loser leaves no trace in
strategies. Directory order is alphabetical by filename, so zz-storm.yml beats
aa-storm.yml.
# storm-farm.yml # storm-boss.yml
name: storm name: storm # ← one of these two is gone
This is the single easiest way to lose a strategy, and it usually happens by copying a working
file and editing only the phases. Change name: first.
strategies validate prints each file's path next to the name it resolved to, which is how you
find a collision. Two lines with the same name means one of them is dead.
The console matches names case-insensitively, so Storm and storm also collide there. The
desktop app matches exactly.
Some names are already taken by built-in strategies, and a file claiming one of them does not work the way you expect:
| Name | Console | Desktop app |
|---|---|---|
pass, flee | Built-in wins; your file is unreachable | Your file replaces the built-in |
aoe | Built-in wins; your file is unreachable | Not a built-in; your file is fine |
priority, configurable | Not built-ins; your file is fine | Your file replaces the built-in |
Avoid all five.
description
Free text. Shows up in the console's strategies listing.
school
Metadata only — it does not filter anything. One of fire, ice, storm, myth, life,
death, balance, star, sun, moon, shadow.
clients
Which client aliases may run this strategy.
clients:
- p1
- p3
- Aliases must match
p<number>:p1,p2,p10.wizard-1is a load error, and so is an empty entry. - Normalised to lowercase; duplicates removed.
- Omitted means every client.
- Clients not listed are left alone entirely — not passed, not engaged.
clients: is enforced by the console onlyThe desktop app's trainer never reads this field. There, the strategy runs against whichever
client the automation step points at, clients: or no clients:. See
which host is running it.
enchant
Automatic enchant behaviour, per action family.
enchant:
auto: true
damage: { prefer: epic, fallback: colossal }
mutation: { prefer: shadow }
blade: { prefer: sharpen }
trap: { fallback: potent }
| Key | Applies to |
|---|---|
auto | Enchant damage spells automatically each round |
damage | Damage spells |
mutation | Mutation enchants (e.g. shadow variants) |
blade | Blades and charms |
trap | Traps and wards |
shield | Shields |
heal | Heals |
support | Fallback for non-damage actions with no more specific policy |
prefer / fallback | Legacy global defaults when nested policies are absent |
prefer and fallback are card-name substrings, not exact names. prefer: epic matches any
enchant whose name contains "epic".
auto: true on its own does nothing — it needs a damage: policy, or a legacy top-level
prefer/fallback, to have anything to apply. Full detail, and the rest of the resolution
order, in Enchants.
targets
Defaults used when an action does not name a target.
targets:
default_enemy: boss
default_ally: lowest_health_ally
default_enemy: first, boss, lowest_health, highest_health, most_pips, random.
default_ally: self, lowest_health_ally, minion, or a client alias like p2.
Neither value is checked by the loader — only by the schema, in your editor. A misspelt
default_enemy silently falls back to the first live enemy. A default_ally alias does not
resolve at all on the desktop app. See Targeting.
fallback
What to do when no phase produces an action. pass or flee.
Set it. Without a fallback a round where nothing resolves does nothing, and a duel where nothing ever resolves stalls until the game times you out.
flee is rarely what you want unattended — it aborts the fight and leaves the character
wherever fleeing drops them.
The runner compares the value against flee case-insensitively and treats everything else
as pass, including a typo. fallback: FLEE works; fallback: fleee quietly means pass. The
loader does not check the value — your editor will, from the schema.
retry_failed_casts
Off unless set. When on, a cast whose click never removes the card from hand is replanned and retried in the same round.
retry_failed_casts: true
cast_retry_limit: 2
The signal is exact rather than inferred. The card clicker samples hand size before clicking and waits for it to shrink, so a cast that did not land surfaces as a timeout error. (Deimos compares card populations across rounds, which a draw can mask.)
Only casts retry, and only on a timeout. A pass, flee, draw, or discard whose click
times out is returned to the caller rather than replaying the round.
Each retry re-reads the hand and re-runs phase selection, so a different card may be chosen the second time.
cast_retry_limit
Maximum consecutive retries. Defaults to 2, must be at least 1, and is only consulted when
retry_failed_casts is true. Deimos has no cap, so a permanently failing cast there replans
until combat ends.
phases
Required, and must be non-empty. Each phase needs at least one action.
phases:
emergency:
priority: 100
when:
health_below: 30
actions:
- heal: { target: self, school: life }
- pass
See Phases.
members
A team block. Mutually exclusive with the top-level clients, enchant, targets,
fallback, and school — mixing the two forms is a load error.
The error message only names four of the five:
strategy "x" mixes members: with top-level clients/enchant/targets/fallback; use one form
A top-level school: triggers it too, even though it is not listed and the published schema
allows the combination. If you get this error and cannot see the offending field, check
school:.
See Teams.
What the loader rejects
Validation is strict, and deliberately so. Every message below is what you will actually see,
wrapped in parse <path>: for a YAML shape problem or validate <path>: for a rule violation —
which is a quick way to tell the two apart.
| Rejected | Message |
|---|---|
| Unknown top-level field | line 4: field foo not found in type strategy.StrategyConfig |
| Unknown field in a phase or action mapping | line 9: field cst not found in type strategy.ActionConfig |
No name: and no filename to derive one from | strategy name is required |
members: mixed with top-level shorthand | strategy "x" mixes members: with top-level clients/enchant/targets/fallback; use one form |
A clients entry that is not p<number> | strategy "x" has invalid client target "wizard-1" (expected p1, p2, ...) |
An empty clients entry | strategy "x" has an empty client target |
Missing or empty phases | strategy "x" has no phases |
A phase with no actions | phase "main" in strategy "x" has no actions |
A phase with members: but no members: block | phase "main" in strategy "x" assigns members but no members: block is defined |
| A phase naming a member that does not exist | phase "main" in strategy "x" references unknown member "life" |
| A shared phase whose action targets a hardcoded alias | phase "main" in strategy "x" is shared across members but action 1 targets a hardcoded client alias "p1"; use relative targets (ally/enemy/self) |
| An unknown enchant step type | action 1 in phase "main" of strategy "x" has unknown enchant step type "sharpen" at index 1 |
An unknown swap: category, including aura | action 1 in phase "main" of strategy "x": swap category "aura" is not one of charm, ward or over_time: not found |
A malformed expr: | expr: <reason> at offset <n> |
An unknown expr: attribute | expr: unknown attribute "helth" at offset 5 |
% on an attribute with no max_ counterpart | expr: "level" has no max_level counterpart so % is not supported, at offset 5 |
name: bad
clients:
- wizard-1 # rejected: not p<number>
phases:
main:
typo_actions: # rejected: unknown field
- pass
What the loader does not reject
Just as important, because these are the mistakes that produce a file which loads cleanly and then misbehaves. Your editor catches most of them from the schema; the loader does not.
| Not checked | What happens instead |
|---|---|
A misspelt target: value | Silently resolves to the first live enemy |
A fallback: that is not pass/flee | Treated as pass |
| Two action keys in one list item | Only the first by internal precedence runs |
has_trap: storm (a bare school string) | Treated as has_trap: true — any trap, any school |
A condition school: outside the seven base schools | Treated as "any school" |
A leaf field beside all:/any: on the same node | The leaf is ignored entirely |
| An unrecognised bare scalar action | Parses to an empty action that never resolves |
A name: that duplicates another file's | The other file is silently replaced |
Attaching the schema in your editor is not optional decoration — it is the only thing that catches this list before a fight does.
The strictness applies to action mappings. An unknown action written as a bare scalar is silently swallowed:
actions:
- pss # typo. Parses with no error, produces an empty action.
- {} # also accepted, also does nothing.
Only pass, flee, and draw are recognised scalars; anything else decodes to an empty
action that can never resolve. The file loads, the phase reports one action, and nothing ever
happens — which presents exactly as
"strategy loads but does nothing".
Written as a mapping, the same typo is caught: - cst: {} is a hard error.
Until this is tightened, validate the file — the console's strategies validate — and
confirm the strategy actually casts something in a real fight, on either host.
Authoring rules that pay off
- Always set
name. - Give every phase an explicit, distinct
priority— never rely on map ordering. - Order actions from most specific to least specific.
- Use
clients:only when you deliberately want to scope. - One clear role per file. A catch-all strategy is hard to reason about and harder to debug.
fallback: passunless you really want to flee.- One action key per action object.
- Match targets to spell types.