Skip to main content

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

The name is the key. Duplicates silently overwrite

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.

Reserved names

Some names are already taken by built-in strategies, and a file claiming one of them does not work the way you expect:

NameConsoleDesktop app
pass, fleeBuilt-in wins; your file is unreachableYour file replaces the built-in
aoeBuilt-in wins; your file is unreachableNot a built-in; your file is fine
priority, configurableNot built-ins; your file is fineYour 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-1 is 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 only

The 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 }
KeyApplies to
autoEnchant damage spells automatically each round
damageDamage spells
mutationMutation enchants (e.g. shadow variants)
bladeBlades and charms
trapTraps and wards
shieldShields
healHeals
supportFallback for non-damage actions with no more specific policy
prefer / fallbackLegacy 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.

RejectedMessage
Unknown top-level fieldline 4: field foo not found in type strategy.StrategyConfig
Unknown field in a phase or action mappingline 9: field cst not found in type strategy.ActionConfig
No name: and no filename to derive one fromstrategy name is required
members: mixed with top-level shorthandstrategy "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 entrystrategy "x" has an empty client target
Missing or empty phasesstrategy "x" has no phases
A phase with no actionsphase "main" in strategy "x" has no actions
A phase with members: but no members: blockphase "main" in strategy "x" assigns members but no members: block is defined
A phase naming a member that does not existphase "main" in strategy "x" references unknown member "life"
A shared phase whose action targets a hardcoded aliasphase "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 typeaction 1 in phase "main" of strategy "x" has unknown enchant step type "sharpen" at index 1
An unknown swap: category, including auraaction 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: attributeexpr: unknown attribute "helth" at offset 5
% on an attribute with no max_ counterpartexpr: "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 checkedWhat happens instead
A misspelt target: valueSilently resolves to the first live enemy
A fallback: that is not pass/fleeTreated as pass
Two action keys in one list itemOnly 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 schoolsTreated as "any school"
A leaf field beside all:/any: on the same nodeThe leaf is ignored entirely
An unrecognised bare scalar actionParses to an empty action that never resolves
A name: that duplicates another file'sThe 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.

One gap: unknown scalar actions are accepted

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 validateand confirm the strategy actually casts something in a real fight, on either host.

Authoring rules that pay off

  1. Always set name.
  2. Give every phase an explicit, distinct priority — never rely on map ordering.
  3. Order actions from most specific to least specific.
  4. Use clients: only when you deliberately want to scope.
  5. One clear role per file. A catch-all strategy is hard to reason about and harder to debug.
  6. fallback: pass unless you really want to flee.
  7. One action key per action object.
  8. Match targets to spell types.