Homebrew by hand

Write packs in a plain text editor, check them against the JSON schemas and load them into the app.

Everything the in-app forms can do, and several things they cannot, can be written in an ordinary text editor. A pack is one JSON file. If you are comfortable with JSON, this is the fastest way to write a lot of content, to copy a pattern across twenty entries, or to use features the forms have no box for, such as charges, requirements and conditional rules.

This page assumes you know what JSON is and nothing else. By the end you will have written a pack, checked it, and loaded it into the app.

What you need

  • A text editor that understands JSON. Visual Studio Code, the JetBrains editors, Sublime Text and Neovim all do, and all can use a JSON Schema to catch mistakes as you type. Notepad will work, but will not tell you when you have dropped a comma.
  • The JSON schema files below.
  • A way to get the finished file onto your device: a cloud drive, AirDrop, email, a USB cable.
  • Optional, but recommended: the sample pack, one of each kind of entry, used as the running example on this page.

Get the JSON schemas

A JSON schema describes the shape of a file: which fields exist, which are required and what type each one has. Give one to your editor and it will underline a misspelled field, offer to complete one and explain what each one means.

These three files describe a pack. They are published here, straight from the engine’s own source:

They refer to each other by name, so if you download them keep all three in the same folder. You only ever point your editor at content-compendium.schema.json; it finds the others itself.

Point your editor at the schema

You do not put anything in the pack file itself. Instead you tell the editor “files that end in .comp.json follow this schema”.

Visual Studio Code. Open your user or workspace settings (settings.json) and add:

{
  "json.schemas": [
    {
      "fileMatch": ["*.comp.json"],
      "url": "https://crawler-vault-marketing.web.app/schema/content-compendium.schema.json"
    }
  ]
}

JetBrains editors (IntelliJ IDEA, WebStorm and the rest). Open Settings → Languages & Frameworks → Schemas and DTDs → JSON Schema Mappings, add a mapping, paste the address of content-compendium.schema.json as the schema, choose JSON Schema version 2020-12 and add the file name pattern *.comp.json.

Anything else. Editors that use the JSON language server accept the same json.schemas setting shown above. If your editor wants a local file, download the three schema files into one folder and give it the path to content-compendium.schema.json.

What the schema catches, and what it does not

The schema knows the shape of a pack. It will catch:

  • a misspelled field (predicte, modifer), or a field in the wrong place;
  • a missing required field (every entry needs an id and a doc, every modifier a target, op, value and label);
  • the wrong type: a number where text goes, a list where an object goes;
  • an operator or time unit the game does not have.

It cannot know the meaning of what you wrote. It will happily accept a bonus aimed at a stat that does not exist, a tag the game has never heard of, or a reference to a skill nobody published. For those, run Check… in the app (see Checking your work).

The file

A pack is a JSON file ending in .comp.json (the comp is for compendium). When the app exports a pack it names the file after the pack’s identifier, and you may as well do the same. The smallest useful pack is:

{
  "repo": "my-pack",
  "entries": []
}

Fill in the rest of the details and it looks like this:

{
  "repo": "crawler-samples",
  "name": "Crawler Samples",
  "source": "Crawler Vault help: Homebrew by hand",
  "version": "1.0",
  "description": "One of each kind of entry, written by hand. Copy it, rename it, break it.",
  "authors": ["Crawler Vault"],
  "license": "CC0-1.0",
  "system": "dccarl",
  "entries": []
}
FieldRequiredWhat it is
repoyesThe pack’s permanent identifier. See below.
entriesyesThe list of content.
nameWhat the pack is called on screen. Free to change.
descriptionA sentence shown under the name.
authorsA list of names, because packs get co-written and handed on.
sourceWhere the ideas came from.
licenseWhat anyone else may do with the pack. Use an SPDX identifier such as CC-BY-4.0 or MIT where one fits. Leaving this blank is the wrong default for something meant to be shared, and Check will say so.
licenseUrl, copyrightThe licence’s address, and the copyright line.
versionStamped onto every reference the pack publishes. See Versions.
systemWhich game the pack is for: dccarl.
minSystemVersionThe oldest version of the game’s definition the pack works with.

The repo identifier

repo is half of every reference to your content: a skill in your pack is your-repo/the-skill-id. That has three consequences.

  • It is lowercase letters and digits, in segments joined by . or -: crawler-samples, dave.boom, table-3. No spaces, capitals or underscores.
  • It must be unique. Two packs with the same identifier would publish references that resolve into each other’s content. It also must not be one of the official books (dcc-core).
  • Never change it once anyone is using the pack. A crawler holding old-name/thing will not find it under new-name.

A plain word like homebrew is a poor choice: you may not be the only person who thought of it. Something with your table’s or your own name in it is better.

Anatomy of an entry

Each item in entries has an id and a doc. Here is a ring, complete:

{
  "id": "ring-of-pep",
  "doc": {
    "name": "Ring of Pep",
    "tags": ["trait:accessory", "trait:magical"],
    "rules": [
      { "kind": "Note", "text": "While worn, your Strength is 2 higher." },
      {
        "kind": "Modifier",
        "modifier": {
          "target": { "kind": "exact", "path": "stats.str.score" },
          "op": "Add",
          "value": "2",
          "label": "Ring of Pep",
          "type": "item",
          "predicate": { "kind": "tag", "tag": "item:equipped" }
        }
      }
    ]
  }
}
  • id is what the rest of the game refers to the entry by. Use lowercase letters, digits and hyphens. It must be unique within the pack, and it never changes once a crawler owns one. (A crawler holds a reference to repo/id, and renaming the id breaks it.)
  • doc is the content itself. Its fields:
FieldWhat it is
nameWhat a player reads.
tagsLabels that tell the game what this is and where it goes. See Tags.
rulesWhat it does. A list of rules, in order.
rollDescriptorsDice lines it declares, such as a skill’s damage.
chargesLimited uses, like a wand’s. See Charges, timers and requirements.
timersHow long it lasts, and its cooldown.
requirementsWhat a crawler must be before taking it.
metaNotes for tools: mana cost, range. Never changes what the entry does.

Every field is optional. An entry with only a name is perfectly legal, and so is a piece of gear that does nothing but exist, which describes most of what a crawler picks up.

  • version (next to id, not inside doc) is optional and covered under Versions.

Tags

Tags are how the game finds things. A tag is a short word with a colon in it, like skill:spell or trait:head. They decide which tab an entry lands on, which slot gear fits, and whether a buff is internal or external.

Which tags for which kind

Kind of entryTags
GearOne tag per place it can be worn: trait:head, trait:torso (or trait:armor), trait:arms, trait:hands, trait:legs, trait:feet, trait:holdable, trait:accessory. Add trait:two-handed for two hands’ worth, trait:stackable for ammunition and rations, and trait:magical for flavour.
Any skillskill, so it appears on the Skills tab, and a kind: skill:attack, skill:spell or skill:utility.
Skill extrasskill:passive, skill:interrupt, stat:str (or int, con, dex, cha) for the stat it is rolled with, damage:fire and so on for what it deals, attack:melee / attack:ranged, and skill:target:self, skill:target:person, skill:target:object or skill:target:area.
Buffbuff:internal or buff:external.
Debuffdebuff: plus a name of your own, such as debuff:my-pack.soggy-socks. Two debuffs with the same name apply once, unless they also carry effect:stackable. effect:requires-action means it takes an Action to switch on.
Racerace and race: plus its id.
Classclass and class: plus its id.

Gear’s slots are read from the tags, so tagging an item is how you say where it goes. An item that fits two places carries two tags. The bandanna that goes on your head or your accessory slot carries both.

The vocabulary is checked

The game declares the tags it knows. A tag outside that list is reported by Check, so a typo does not silently do nothing. Two kinds of tag are open-ended and need no declaration, as long as they start with one of these prefixes:

applies-to:, attack:, buff:, check:, class:, debuff:, effect:, grants:, item:, opposed-by:, race:, skill:favored:, spell:, stat:, target:, trait:, situation:, self:condition:

So trait:cursed-by-a-cat is fine and house:banned is a problem. The tags that are fixed in advance are the skill, class, race and similar words above, the thirteen damage types, and the three ways to answer them:

  • Damage types: acid, bludgeoning, electric, fire, force, holy, ice, necrotic, piercing, poison, psychic, slashing and sonic, written damage:fire.
  • Resistances: resist:fire, immune:fire and vulnerable:fire, for any of those thirteen. Resistance halves the damage, vulnerability doubles it and immunity takes it to nothing. resist:water is an error.

Rules

rules is the list of things an entry does. Each rule is an object with a kind. The four you will use nearly all the time are Note, Modifier, GrantTag and GrantEntry.

Note

Words for the player to read. This is how the books are written, and how a rule the sheet cannot compute is still recorded.

{ "kind": "Note", "text": "Melee Attack, Str. Put something heavy in a sock. Swing the sock." }

A note can wait for a condition with a predicate. It appears only while the condition is true:

{
  "kind": "Note",
  "text": "Rank 5: +1d4 base damage.",
  "predicate": { "kind": "expr", "expr": "@source.rank >= 5" }
}

The field is called predicate. Do not write when, which is what the game’s own definition uses and is easy to mix up. The schema rejects it, which is the point of having the schema.

Modifier

A modifier changes a number. It is the heart of a ring, a race or a buff.

{
  "kind": "Modifier",
  "modifier": {
    "target": { "kind": "exact", "path": "stats.str.score" },
    "op": "Add",
    "value": "2",
    "label": "Ring of Pep",
    "type": "item",
    "predicate": { "kind": "tag", "tag": "item:equipped" }
  }
}
FieldWhat it does
targetWhat to change. Usually { "kind": "exact", "path": "..." }. A { "kind": "pattern", "glob": "skill.*.rank" } hits every attribute that matches, where * stands for exactly one segment of the path.
opWhat to do to it. See below.
valueHow much, written as an expression. Always a string: "2", "-1", "@character.floor".
labelWhat the sheet shows in the breakdown when a player asks “why is my Strength 8?”.
typeWhere it comes from: item, race, class, status and so on. Leave it out for an untyped modifier. In this game modifiers stack.
predicateWhen it applies. Left out, it always does.
priorityExplicit precedence when two modifiers compete. Ties fall to the order they were written, later winning. Races and classes use it so that a class’s changes beat a race’s.

The operators:

opDoes
AddAdds the value. A negative value subtracts. By far the most common.
MultiplyMultiplies a plain number: "0.5" halves a Speed.
OverrideSets it to the value, whatever it was.
UpgradeSets it to at least the value: a floor rather than a cap.
DowngradeSets it to at most the value.
AddDiceAdds dice to a dice line: "dice(1, 6)". A damage line takes this.
MultiplyBaseDiceMultiplies the rolled base damage before anything is added.
MultiplyTotalDiceMultiplies the finished damage: “x2 the total”.

UpgradeRank, DowngradeRank, SetDieCount, UpgradeDieSize, GrantFlag and ClearFlag also exist for corners of the system most packs never reach.

The numbers you can change in Dungeon Crawler Carl:

PathWhat it is
stats.str.score, stats.int.score, stats.con.score, stats.dex.score, stats.cha.scoreA stat’s enhanced value: base plus everything on you. Aim at .score, never .base; the base is the number the player writes and nothing else may touch it. The modifier (.mod) follows from the score on its own.
defense.armorArmor. Adds to the Damage Resistance total.
defense.dr.buffsDamage reduction from buffs. Use this for a potion or a skill, and leave armor to armor.
evasion.buffsAdded to Evade.
movement.speed, movement.stepSpeed and Step, after everything.
check.modifierAdded to every Check. Soggy Socks use it.
skill.<id>.rankA skill’s rank. skill.*.rank is every skill the crawler has.
skill.<id>.max-rankThe highest rank a skill can reach.
skill.<id>.damageA skill’s damage line (dice).
skill.<id>.extra-manaExtra mana a skill costs.
debuff.endOfRound.damageDamage taken at the end of each round (dice).
regen.hp.perMinute, regen.mana.perMinuteRegeneration.
body.arms, body.hands, body.legs, body.feet, body.headsHow many of each a crawler has, which sets how many things they can hold or wear.
character.sizeThe crawler’s size. Races set it with Override and a value like "#size:large".

A path that does not exist would do nothing at all, silently, which is the worst kind of bug. That is what Check is for: it reports a modifier aimed at an attribute the game does not have.

GrantTag

Gives the crawler a tag while the entry applies. This is how resistances work:

{
  "kind": "GrantTag",
  "tag": "resist:fire",
  "predicate": { "kind": "tag", "tag": "item:equipped" }
}

A race grants origin:earth or origin:alien the same way, and a class grants its class-type: tags.

GrantEntry

Hands the crawler another piece of content: a skill, a spell, a buff. The crawler then has it, just as if they had learned it.

{
  "kind": "GrantEntry",
  "ref": { "repo": "crawler-samples", "id": "chefs-kiss" },
  "bind": { "rank": "0" },
  "retain": true
}
FieldWhat it does
refWhich entry: its repo and id. A version is optional, and leaving it out means “whatever the pack has”.
bindThe entry’s starting state, as expressions. { "rank": "7" } starts a granted skill at rank 7.
appliesWhileFor gear: "EQUIPPED" (the default) or "CARRIED" for something that applies while it is merely in the pack.
predicateWhen the grant holds. It is withdrawn when this stops being true.
retaintrue means that what was granted stays the crawler’s own when the grant ends. Without it, it goes away.

Granting says what the crawler gets. How good it is belongs to a modifier on the rank, as in the Line Cook below. Keeping the two apart lets a grant be skipped for a crawler who already has the skill, while the bonus still applies.

ref must point at an entry that exists in a pack that is switched on. Check reports a grant whose target nothing publishes.

Predicates and expressions

Two small languages sit inside the quotation marks.

A predicate says when. It is an object with a kind:

kindMeans
tagThe crawler has this tag. { "kind": "tag", "tag": "item:equipped" } is the one for gear.
exprThis expression is true. { "kind": "expr", "expr": "@source.rank >= 5" }
and, orAll, or any, of the predicates in of.
notThe opposite of the predicate in of.
alwaysAlways. The same as leaving it out.

item:equipped is a tag the game works out for you from the crawler’s slots. Gating everything an item grants on it is what makes a ring in a pocket do nothing.

An expression says how much, or whether. The forms the shipped content uses are:

WriteTo mean
2, -1A number.
@stats.str.modThe value of a number on the crawler. Any path from the table above works, plus @character.floor and @character.level.
@source.rankThe rank of the entry this rule belongs to. A skill’s rules read this a lot.
@source.chargesA counter on the entry (see charges, below).
a + b, a - b, a * bArithmetic.
a >= b, a < bComparisons.
cond ? x : yChoose one: @source.rank >= 5 ? dice(1, 4) : 0.
dice(1, 6)One six-sided die. dice(2, 8) is two eight-siders.
flat(@character.floor)A plain number added to a dice line.
hasTag('trait:two-handed')Whether the crawler has a tag.

Writing each kind of entry

The sample pack has one of each. Every block below is copied from it unchanged.

Gear

Gear is a name, tags saying where it goes, and rules gated on item:equipped. The Ring of Pep above adds to a stat. These oven mitts do two things at once: they add armor and make the wearer resist fire.

{
  "id": "oven-mitts",
  "doc": {
    "name": "Oven Mitts of Mild Immolation",
    "tags": ["trait:hands", "trait:magical"],
    "rules": [
      { "kind": "Note", "text": "Fire does half damage to you while these are on. Your hands, anyway." },
      {
        "kind": "Modifier",
        "modifier": {
          "target": { "kind": "exact", "path": "defense.armor" },
          "op": "Add",
          "value": "1",
          "label": "Oven Mitts of Mild Immolation",
          "type": "item",
          "predicate": { "kind": "tag", "tag": "item:equipped" }
        }
      },
      {
        "kind": "GrantTag",
        "tag": "resist:fire",
        "predicate": { "kind": "tag", "tag": "item:equipped" }
      }
    ]
  }
}

Both rules are gated, so the mitts do nothing from a pocket. type is item on the bonus so the sheet credits the right source. trait:hands is what makes the Hands slot offer them.

An attack skill

An attack is a skill with skill:attack, a stat tag and a rollDescriptors line for its damage:

{
  "id": "sock-slap",
  "doc": {
    "name": "Sock Slap",
    "tags": [
      "skill",
      "skill:attack",
      "skill:attack:melee",
      "attack:melee",
      "skill:weapon",
      "damage:bludgeoning",
      "stat:str"
    ],
    "rules": [
      { "kind": "Note", "text": "Melee Attack, Str. Put something heavy in a sock. Swing the sock." },
      {
        "kind": "Note",
        "text": "Rank 5: +1d4 base damage.",
        "predicate": { "kind": "expr", "expr": "@source.rank >= 5" }
      }
    ],
    "rollDescriptors": [
      {
        "id": "damage",
        "dice": [{ "count": 1, "faces": 4 }],
        "bonus": "@stats.str.mod + (@source.rank >= 5 ? dice(1, 4) : 0)",
        "tags": ["damage:bludgeoning"]
      }
    ]
  }
}
  • rollDescriptors declares a dice line with an id of damage. dice is the base dice (count dice with faces sides); bonus is an expression added to it, and tags is its damage type.
  • Here bonus adds the Strength modifier, and +1d4 once the skill is rank 5. A rank-gated step in bonus is how “Rank 5: +1d4 base damage” gets into the numbers, and the matching Note is how it gets onto the page for the player.
  • The stat the damage uses (@stats.str.mod) is separate from the stat the check is rolled with (stat:str). A shuriken is thrown with Dexterity and hurts with Strength.

A spell

A spell is the same shape with skill:spell, and costs mana. The cost and range are not rules, they are notes in meta:

{
  "id": "chefs-kiss",
  "doc": {
    "name": "Chef's Kiss",
    "tags": ["skill", "skill:spell", "stat:int", "damage:fire", "skill:target:person"],
    "rules": [
      { "kind": "Note", "text": "A flick of the fingers and something is, regrettably, on fire." },
      {
        "kind": "Note",
        "text": "Rank 5: +1d6 base damage.",
        "predicate": { "kind": "expr", "expr": "@source.rank >= 5" }
      }
    ],
    "rollDescriptors": [
      {
        "id": "damage",
        "dice": [{ "count": 1, "faces": 6 }],
        "bonus": "@stats.int.mod + (@source.rank >= 5 ? dice(1, 6) : 0)",
        "tags": ["damage:fire"]
      }
    ],
    "meta": { "dccarl.mana-cost": "8", "dccarl.range": "30" }
  }
}

dccarl.mana-cost is the cost in mana, as text. dccarl.range is "self" or a distance in feet. A passive spell carries skill:passive, and one you can cast on someone else’s turn carries skill:interrupt. To give a spell a duration or a cooldown, use timers (below).

A utility skill

A skill that does not hit anything is just notes, gated by rank:

{
  "id": "lucky-socks",
  "doc": {
    "name": "Lucky Socks",
    "tags": ["skill", "skill:utility", "skill:passive", "stat:dex"],
    "rules": [
      { "kind": "Note", "text": "You never lose a sock. Nobody knows how." },
      {
        "kind": "Note",
        "text": "Rank 5: you can always find a clean pair when you need one.",
        "predicate": { "kind": "expr", "expr": "@source.rank >= 5" }
      }
    ]
  }
}

skill:utility puts it with the utility skills, and stat:dex is the stat its checks use. Drop the stat: tag for a skill that is not rolled at all.

A buff

A buff is tagged buff:internal or buff:external, changes a number and says how long it lasts:

{
  "id": "sugar-rush",
  "doc": {
    "name": "Sugar Rush",
    "tags": ["buff:internal"],
    "rules": [
      { "kind": "Note", "text": "Your Speed is 5 higher until the crash." },
      {
        "kind": "Modifier",
        "modifier": {
          "target": { "kind": "exact", "path": "movement.speed" },
          "op": "Add",
          "value": "5",
          "label": "Sugar Rush"
        }
      }
    ],
    "timers": [{ "key": "duration", "amount": 3, "unit": "rounds" }]
  }
}

Internal buffs start switched on. External buffs wait for the player and only three may be on at once. There is no item:equipped predicate here: a buff applies whenever it is switched on.

A debuff

A debuff’s tag is debuff: plus a name of your own. The convention, which the forms follow, is to put your pack’s identifier in the name so two packs cannot collide:

{
  "id": "soggy-socks",
  "doc": {
    "name": "Soggy Socks",
    "tags": ["debuff:crawler-samples.soggy-socks"],
    "rules": [
      { "kind": "Note", "text": "-1 to every Check. Squelch." },
      {
        "kind": "Modifier",
        "modifier": {
          "target": { "kind": "exact", "path": "check.modifier" },
          "op": "Add",
          "value": "-1",
          "label": "Soggy Socks",
          "type": "status"
        }
      }
    ],
    "timers": [{ "key": "duration", "text": "until you change your socks" }]
  }
}

The duration here is words rather than a number. When it can be counted down, give an amount and a unit instead (see timers, below). Add "effect:stackable" to the tags if two of them should pile up.

A race

A race grants an origin, changes stats and can carry any other rules. Races are tagged race and race: plus their id:

{
  "id": "gutter-goblin",
  "doc": {
    "name": "Gutter Goblin",
    "tags": ["race", "race:gutter-goblin"],
    "rules": [
      { "kind": "GrantTag", "tag": "origin:alien" },
      { "kind": "Note", "text": "Small, quick and extremely hard to get rid of." },
      {
        "kind": "Modifier",
        "modifier": {
          "target": { "kind": "exact", "path": "stats.dex.score" },
          "op": "Add",
          "value": "2",
          "label": "Gutter Goblin",
          "type": "race"
        }
      },
      {
        "kind": "Modifier",
        "modifier": {
          "target": { "kind": "exact", "path": "stats.cha.score" },
          "op": "Add",
          "value": "-1",
          "label": "Gutter Goblin",
          "type": "race"
        }
      }
    ]
  }
}

The official races are built from Build Points, and the in-app builder keeps its choices in meta. A race written by hand needs none of that. Open it in the builder later and each rule appears as a benefit (or a custom one), so nothing is lost.

A class

A class is tagged class and class: plus its id, grants a class-type: tag (the official types are arcanist, barbarian, bard, cleric, druid, fighter, mage, merchant, monk, necromancer, paladin and rogue) and, typically, grants skills. This Line Cook grants Chef’s Kiss, then raises its rank:

{
  "id": "line-cook",
  "doc": {
    "name": "Line Cook",
    "tags": ["class", "class:line-cook"],
    "rules": [
      { "kind": "GrantTag", "tag": "class-type:merchant" },
      { "kind": "Note", "text": "You have survived the dinner rush. A dungeon is only a longer shift." },
      {
        "kind": "Modifier",
        "modifier": {
          "target": { "kind": "exact", "path": "stats.con.score" },
          "op": "Add",
          "value": "3",
          "label": "Line Cook",
          "type": "class",
          "priority": 1
        }
      },
      {
        "kind": "GrantEntry",
        "ref": { "repo": "crawler-samples", "id": "chefs-kiss" },
        "bind": { "rank": "0" },
        "retain": true
      },
      {
        "kind": "Modifier",
        "modifier": {
          "target": { "kind": "exact", "path": "skill.chefs-kiss.rank" },
          "op": "Add",
          "value": "3",
          "label": "Line Cook",
          "type": "class",
          "priority": 1
        }
      }
    ]
  }
}

Look at the two halves of the skill. The GrantEntry gives the crawler Chef’s Kiss at rank 0, and retain says that if the class is ever changed, they keep it. The second Modifier adds three ranks to it while they are a Line Cook. priority makes a class’s changes win over a race’s.

Charges, timers and requirements

These three fields sit in doc, beside rules.

Charges

A charge is a counter: arrows in a quiver, castings of a ring.

"charges": [
  { "key": "doses", "max": "3", "resetOn": [] }
]
  • key names it, and is also what the sheet calls it. It is read in the entry’s rules as @source.doses.
  • max is an expression, so "@stats.int.mod" is a legal maximum and follows whoever holds the thing.
  • resetOn lists the rests that refill it. This game has two: rest:safe-room and encounter. An empty list, like the one above, is a consumable that nothing refills.

A note can react to the counter. This line appears only on a spent item:

{
  "kind": "Note",
  "text": "Empty.",
  "predicate": { "kind": "expr", "expr": "@source.doses <= 0" }
}

Charges count uses of one thing. For how many of a thing you have, tag it trait:stackable and the item gets a quantity.

Timers

A timer says how long an effect lasts, or how long before it can be used again.

"timers": [
  { "key": "duration", "amount": 3, "unit": "rounds" },
  { "key": "cooldown", "text": "once per floor" }
]

key is duration or cooldown. Give it an amount with a unit (seconds, rounds, minutes, hours or days) and the sheet counts it down. Give it text and it is shown as words. An amount without a unit is an error. Both together is allowed (“2 rounds or 20 seconds”): the count is for tracking and the words are for reading.

Requirements

A requirement says what a crawler must be before they can take the entry. Each has the words a player sees when they fall short, and a test written as an expression:

"requirements": [
  {
    "text": "Needs a Strength or Dexterity skill at rank 5",
    "when": "maxOf(entries(tag('stat:str') || tag('stat:dex')), 'rank') >= 5"
  }
]

entries(...) is the crawler’s own list of skills and other content, and 'rank' is the rank they trained it to, which is not what their gear gives them. To test a stat, read @stats.str.base rather than the enhanced value, so a bonus the entry itself grants can never qualify the crawler for it. A requirement the game cannot decide counts as unmet. The official Hobgoblin race uses these; open it in the Compendium to see.

Versions and updates

Each reference to your content can carry a version: crawler-samples/ring-of-pep@1.0. The pack’s top-level version is stamped onto every entry, and an entry may state its own version beside id if it has moved on without the rest.

A crawler carries a copy of what it picked up and remembers the version it came from. When you change an entry, change the version too. Crawlers carrying the old one are then offered the update under Updates in their menu. Change the content and leave the version alone, and the game calls it drifted: it knows the text no longer matches, but cannot tell anyone there is anything to apply.

{
  "id": "ring-of-pep",
  "version": "1.1",
  "doc": { "name": "Ring of Pep" }
}

(The forms do this for you: every save in the app stamps a new version.)

Checking your work

There are three layers, from cheapest to most thorough. Use all of them.

  1. Your editor and the schema catch shape errors as you type: a misspelled field, a missing label, a timer in an unknown unit. Red squiggles mean something is wrong before you ever save.
  2. Check… in the app catches meaning errors: it reads the whole pack with the game’s own rules and reports what cannot work. Import the pack, open its ⋮ menu and choose Check…. Problems are bugs. “Worth saying” is a pack that works but is missing a licence or an author.
  3. Try it on a crawler. Add the gear, learn the skill, switch the buff on, and tap the ⓘ beside the number it should have changed. If the breakdown does not mention your entry, it is not doing anything.

The usual mistakes

You wroteWhat Check saysWhat to do
"path": "stats.strr.score"'ring-of-pep' rule 2 ('Ring of Pep') targets unknown attribute 'stats.strr.score'Fix the spelling. Paths are in the table above.
"tag": "resist:water", or "house:banned" in tagsdeclares undeclared tag 'resist:water'Use a declared tag, or one that starts with an open prefix.
"when": {…} on a note'entries[2].doc.rules[1].when' is not a field anything readsThe field is predicate.
A GrantEntry to an entry nobody hasgrants 'dcc-core/nope', which nothing here publishesFix the repo and id, or publish the target.
Two entries with the same idcompendium 'x' declares 'ring-of-pep' more than onceGive each its own.
A kind that is not a rule kind ("Shout")The file cannot be read at all, and Check names the position.Use Note, Modifier, GrantTag or GrantEntry.
"repo": "My Pack"repo 'My Pack' is not a namespace…Lowercase letters, digits, - and . only.

Two others are silent, and only testing finds them. A bonus aimed at .base instead of .score applies, but where a player cannot see it. A gear rule without the item:equipped predicate applies from a pocket.

Getting your pack into the app

  1. Save the file with a name that ends in .comp.json.
  2. Get it onto the device: AirDrop it, put it in a cloud drive, email it to yourself or copy it across with a cable.
  3. In the app, open the Homebrew tab, tap the Import a pack icon and choose the file.
  4. If a pack with the same repo is already there, the app asks whether to Replace it or Keep both. Replace is what you want when you are working on a pack and have re-saved it.
  5. Open the pack, run Check… and fix whatever it reports.

Importing does not check every entry for you, so run Check. If an entry turns out to be wrong, you can edit it in the app without leaving, Edit as text… puts you back in JSON.

Writing in both places

The app and your editor are working on the same file, so nothing stops you using both. The usual loop is:

  1. Make the pack in the app and add an entry or two with the forms.
  2. Export… it, and open the file in your editor.
  3. Write twenty more entries by copying the first one.
  4. Import it again, and choose Replace. Crawlers carrying the old versions are offered the updates.

The complete sample pack

Here is the whole sample pack, the same file as the download.

crawler-samples.comp.json
{
  "repo": "crawler-samples",
  "name": "Crawler Samples",
  "source": "Crawler Vault help: Homebrew by hand",
  "version": "1.0",
  "description": "One of each kind of entry, written by hand. Copy it, rename it, break it.",
  "authors": ["Crawler Vault"],
  "license": "CC0-1.0",
  "system": "dccarl",
  "entries": [
    {
      "id": "ring-of-pep",
      "doc": {
        "name": "Ring of Pep",
        "tags": ["trait:accessory", "trait:magical"],
        "rules": [
          { "kind": "Note", "text": "While worn, your Strength is 2 higher." },
          {
            "kind": "Modifier",
            "modifier": {
              "target": { "kind": "exact", "path": "stats.str.score" },
              "op": "Add",
              "value": "2",
              "label": "Ring of Pep",
              "type": "item",
              "predicate": { "kind": "tag", "tag": "item:equipped" }
            }
          }
        ]
      }
    },
    {
      "id": "oven-mitts",
      "doc": {
        "name": "Oven Mitts of Mild Immolation",
        "tags": ["trait:hands", "trait:magical"],
        "rules": [
          { "kind": "Note", "text": "Fire does half damage to you while these are on. Your hands, anyway." },
          {
            "kind": "Modifier",
            "modifier": {
              "target": { "kind": "exact", "path": "defense.armor" },
              "op": "Add",
              "value": "1",
              "label": "Oven Mitts of Mild Immolation",
              "type": "item",
              "predicate": { "kind": "tag", "tag": "item:equipped" }
            }
          },
          {
            "kind": "GrantTag",
            "tag": "resist:fire",
            "predicate": { "kind": "tag", "tag": "item:equipped" }
          }
        ]
      }
    },
    {
      "id": "sock-slap",
      "doc": {
        "name": "Sock Slap",
        "tags": [
          "skill",
          "skill:attack",
          "skill:attack:melee",
          "attack:melee",
          "skill:weapon",
          "damage:bludgeoning",
          "stat:str"
        ],
        "rules": [
          { "kind": "Note", "text": "Melee Attack, Str. Put something heavy in a sock. Swing the sock." },
          {
            "kind": "Note",
            "text": "Rank 5: +1d4 base damage.",
            "predicate": { "kind": "expr", "expr": "@source.rank >= 5" }
          }
        ],
        "rollDescriptors": [
          {
            "id": "damage",
            "dice": [{ "count": 1, "faces": 4 }],
            "bonus": "@stats.str.mod + (@source.rank >= 5 ? dice(1, 4) : 0)",
            "tags": ["damage:bludgeoning"]
          }
        ]
      }
    },
    {
      "id": "chefs-kiss",
      "doc": {
        "name": "Chef's Kiss",
        "tags": ["skill", "skill:spell", "stat:int", "damage:fire", "skill:target:person"],
        "rules": [
          { "kind": "Note", "text": "A flick of the fingers and something is, regrettably, on fire." },
          {
            "kind": "Note",
            "text": "Rank 5: +1d6 base damage.",
            "predicate": { "kind": "expr", "expr": "@source.rank >= 5" }
          }
        ],
        "rollDescriptors": [
          {
            "id": "damage",
            "dice": [{ "count": 1, "faces": 6 }],
            "bonus": "@stats.int.mod + (@source.rank >= 5 ? dice(1, 6) : 0)",
            "tags": ["damage:fire"]
          }
        ],
        "meta": { "dccarl.mana-cost": "8", "dccarl.range": "30" }
      }
    },
    {
      "id": "lucky-socks",
      "doc": {
        "name": "Lucky Socks",
        "tags": ["skill", "skill:utility", "skill:passive", "stat:dex"],
        "rules": [
          { "kind": "Note", "text": "You never lose a sock. Nobody knows how." },
          {
            "kind": "Note",
            "text": "Rank 5: you can always find a clean pair when you need one.",
            "predicate": { "kind": "expr", "expr": "@source.rank >= 5" }
          }
        ]
      }
    },
    {
      "id": "sugar-rush",
      "doc": {
        "name": "Sugar Rush",
        "tags": ["buff:internal"],
        "rules": [
          { "kind": "Note", "text": "Your Speed is 5 higher until the crash." },
          {
            "kind": "Modifier",
            "modifier": {
              "target": { "kind": "exact", "path": "movement.speed" },
              "op": "Add",
              "value": "5",
              "label": "Sugar Rush"
            }
          }
        ],
        "timers": [{ "key": "duration", "amount": 3, "unit": "rounds" }]
      }
    },
    {
      "id": "soggy-socks",
      "doc": {
        "name": "Soggy Socks",
        "tags": ["debuff:crawler-samples.soggy-socks"],
        "rules": [
          { "kind": "Note", "text": "-1 to every Check. Squelch." },
          {
            "kind": "Modifier",
            "modifier": {
              "target": { "kind": "exact", "path": "check.modifier" },
              "op": "Add",
              "value": "-1",
              "label": "Soggy Socks",
              "type": "status"
            }
          }
        ],
        "timers": [{ "key": "duration", "text": "until you change your socks" }]
      }
    },
    {
      "id": "gutter-goblin",
      "doc": {
        "name": "Gutter Goblin",
        "tags": ["race", "race:gutter-goblin"],
        "rules": [
          { "kind": "GrantTag", "tag": "origin:alien" },
          { "kind": "Note", "text": "Small, quick and extremely hard to get rid of." },
          {
            "kind": "Modifier",
            "modifier": {
              "target": { "kind": "exact", "path": "stats.dex.score" },
              "op": "Add",
              "value": "2",
              "label": "Gutter Goblin",
              "type": "race"
            }
          },
          {
            "kind": "Modifier",
            "modifier": {
              "target": { "kind": "exact", "path": "stats.cha.score" },
              "op": "Add",
              "value": "-1",
              "label": "Gutter Goblin",
              "type": "race"
            }
          }
        ]
      }
    },
    {
      "id": "line-cook",
      "doc": {
        "name": "Line Cook",
        "tags": ["class", "class:line-cook"],
        "rules": [
          { "kind": "GrantTag", "tag": "class-type:merchant" },
          { "kind": "Note", "text": "You have survived the dinner rush. A dungeon is only a longer shift." },
          {
            "kind": "Modifier",
            "modifier": {
              "target": { "kind": "exact", "path": "stats.con.score" },
              "op": "Add",
              "value": "3",
              "label": "Line Cook",
              "type": "class",
              "priority": 1
            }
          },
          {
            "kind": "GrantEntry",
            "ref": { "repo": "crawler-samples", "id": "chefs-kiss" },
            "bind": { "rank": "0" },
            "retain": true
          },
          {
            "kind": "Modifier",
            "modifier": {
              "target": { "kind": "exact", "path": "skill.chefs-kiss.rank" },
              "op": "Add",
              "value": "3",
              "label": "Line Cook",
              "type": "class",
              "priority": 1
            }
          }
        ]
      }
    }
  ]
}

What is not covered here

This page is about content: the things that go into a pack. It does not cover writing a new system definition, which is what decides the tags, stats and paths above. The official books also use some rules this page does not describe (choice sets, granted entities and per-entry state). The best source of examples is the Compendium in the app, which shows the real thing.