← Back to dashboard

The campaign file format

The Keep loads a campaign from a single JSON file. Here's the format — and a prompt you can hand to any AI to generate one for you.

Save a file ending in .json, then on your dashboard click Import from file. It opens in the editor so you can review everything before you use it — nothing is overwritten, and any broken reference is flagged for you to fix.

Generate one with AI

Don't want to write JSON by hand? Paste this brief into any AI tool (ChatGPT, Claude, Gemini, a local model), then describe your realm in plain language — the holdfasts, the knights, who is allied or feuding, and the secrets players don't know yet. It returns a file in exactly this format.

Paste this into your AI agent
You are helping build an importable campaign file for "The Keep", a tabletop
RPG NPC & relationship tracker for game masters. Produce ONE JSON document in
exactly the format below. Output only the JSON (no prose, no markdown fences).
It must be valid JSON and will be saved as a .json file and loaded into the app.

== TOP-LEVEL SHAPE ==
{
  "format": "the-keep/campaign",   // include verbatim
  "version": 1,                     // include verbatim
  "meta":  { ... },
  "GROUPS": { ... },                // REQUIRED. an object keyed by group id
  "EDGE_TYPES": { ... },            // REQUIRED. an object keyed by edge id
  "npcs": [ ... ],                  // REQUIRED. an array
  "relationships": [ ... ]          // an array (may be empty)
}
GROUPS and EDGE_TYPES are OBJECTS keyed by a short slug id (lowercase, a-z 0-9
and hyphens). npcs and relationships are ARRAYS.

== meta ==
  title           string   campaign name
  blurb           string   optional one-line description
  playerMapAccess boolean  optional; whether players may see the map. default false

== GROUPS — { "<groupId>": { ... } } ==
A group is a faction / holdfast / place / myth that NPCs belong to.
  label   string   required. display name, e.g. "Holdfast of Aldermarch"
  kind    string   one of: "pcs" | "holding" | "place" | "myth" | "other"
                   (free text allowed, but these drive the map drawing style)
  accent  string   required. hex colour like "#7a2f2f"
  letter  string   optional. a single map label letter, e.g. "A"
  note    string   optional. short sub-label
  hidden  boolean  optional. true = referee-only; hidden from players entirely
  representation  string  optional override of the map style; one of
                  "holding" | "cartouche" | "myth" | "banner" | "waypoint" | "warband".
                  Omit it unless you have a reason — it is derived from kind.
Always include a group with kind "pcs" for the player characters.

== EDGE_TYPES — { "<edgeId>": { ... } } ==
Reusable relationship categories.
  label  string  required. e.g. "bonded", "rival", "in debt to"
  color  string  required. hex colour

== npcs — [ { ... } ] ==
  id        string    REQUIRED. unique slug (lowercase-hyphen). stable key used
                      by relationships and "met".
  name      string    REQUIRED. display name
  role      string    short title, e.g. "Reeve of Aldermarch"
  group     string    REQUIRED. must equal one of the GROUPS keys
  facts     string[]  things PLAYERS may know. use [] if none
  status    string    "alive" | "dead" | "dead-revenant" | "wounded" |
                      "missing" | "unknown" (free text allowed)
  isPC      boolean   optional. true for player characters (put them in the pcs group)
  steed     string    optional
  alias     string    optional "also known as"
  hiddenFacts string[]  optional. REFEREE-ONLY secrets. never shown to players
  met       string[]  optional. ids of PCs who have met this NPC (referee tracking)
  metQualifier string   optional. e.g. "only Sir Aldous", "while disguised"
  hiddenEntirely     boolean  optional. true = NPC does not appear in the player view at all
  aliasInPlayerView  string   optional. name players see instead of the real one
  roleInPlayerView   string   optional. role players see instead
  groupInPlayerView  string   optional. a GROUPS key players see instead
  factsInPlayerView  string[] optional. if set, REPLACES facts for players

== relationships — [ { ... } ] ==
  from        string  REQUIRED. an npc id
  to          string  REQUIRED. an npc id
  type        string  REQUIRED. an EDGE_TYPES key
  visibility  string  REQUIRED. "known" (players see it) | "partial" (players
                      see a connection but not the detail) | "hidden" (referee-only)
  detail      string  optional. one line on the relationship
  detailInPlayerView string  optional. detail players see instead

== HARD RULES (the import is rejected or flagged otherwise) ==
1. Output valid JSON only.
2. GROUPS, npcs, and EDGE_TYPES must all be present.
3. Every id is unique within its collection (npc ids, group ids, edge ids).
4. Every npc.group (and groupInPlayerView) must match an existing GROUPS key.
5. Every relationship.from / relationship.to must match an existing npc id.
6. Every relationship.type must match an existing EDGE_TYPES key.
7. Every id in an npc's "met" list must match an existing npc id.

== PRIVACY MODEL (so secrets land correctly) ==
The referee sees everything; players see a scrubbed view. Player-safe content:
"facts", relationships with visibility "known"/"partial", and the *InPlayerView
substitutions. Referee-only content: "hiddenFacts", relationships with
visibility "hidden", any group or npc marked hidden/hiddenEntirely. Put spoilers
in the referee-only fields, never in "facts".

== COMPLETE EXAMPLE (valid, self-consistent — match this structure) ==
{
  "format": "the-keep/campaign",
  "version": 1,
  "meta": {
    "title": "The Marches of Vurn",
    "blurb": "A small border realm of feuding holdfasts.",
    "playerMapAccess": false
  },
  "GROUPS": {
    "pcs":        { "label": "The Errant Company",   "kind": "pcs",     "accent": "#6b4a2b" },
    "aldermarch": { "label": "Holdfast of Aldermarch","kind": "holding", "accent": "#7a2f2f", "letter": "A" },
    "hollow":     { "label": "The Hollow Court",      "kind": "myth",    "accent": "#5a3a5a", "hidden": true }
  },
  "EDGE_TYPES": {
    "bond":  { "label": "bonded",      "color": "#2f6b4a" },
    "rival": { "label": "rival",       "color": "#a05a2c" },
    "debt":  { "label": "in debt to",  "color": "#5a3a5a" }
  },
  "npcs": [
    {
      "id": "sela",
      "name": "Sela the Wright",
      "role": "Knight-errant",
      "group": "pcs",
      "isPC": true,
      "status": "alive",
      "steed": "a grey courser",
      "facts": ["Carries her mother's smith-hammer.", "Swore an oath at the Drowned Shrine."]
    },
    {
      "id": "marl",
      "name": "Reeve Marl",
      "role": "Reeve of Aldermarch",
      "group": "aldermarch",
      "status": "alive",
      "facts": ["Keeps the holdfast's tithes and tallies."],
      "hiddenFacts": ["Skims grain to pay a debt to the Hollow Court."]
    },
    {
      "id": "envoy",
      "name": "The Pale Envoy",
      "role": "Messenger of the Hollow Court",
      "group": "hollow",
      "status": "unknown",
      "facts": [],
      "hiddenEntirely": true
    }
  ],
  "relationships": [
    { "from": "sela", "to": "marl",  "type": "bond", "visibility": "known",  "detail": "The Company guards Aldermarch's road." },
    { "from": "marl", "to": "envoy", "type": "debt", "visibility": "hidden", "detail": "Owes the Hollow Court for last winter's grain." }
  ]
}

Now generate the campaign file for the realm I describe.

The format at a glance

A campaign file has a few parts. GROUPS and EDGE_TYPES are objects keyed by a short id; npcs and relationships are lists.

KeyWhat it is
metaTitle, optional blurb, and whether players may see the map.
GROUPSThe factions, holdfasts, places and myths NPCs belong to. Keyed by id. Always include one with kind: "pcs" for the player characters.
EDGE_TYPESReusable relationship categories (bonded, rival, sworn to…) with a colour.
npcsThe people. Each has a unique id, a group, player-visible facts, and optional referee-only hiddenFacts.
relationshipsLinks between two NPCs, by their ids, with a type and a visibility.

What players see vs. what only you see

Players get a scrubbed view of the campaign. Keep spoilers out of the player-safe fields:

Players can seeReferee only
facts, relationships with visibility"known" or "partial", and any …InPlayerView substitutes.hiddenFacts, relationships with visibility: "hidden", groups with hidden: true, and NPCs with hiddenEntirely: true.

A complete example

This is a valid file: every NPC's group exists, every relationship points at real NPCs and a real edge type. It's the same example included in the agent instructions above.

my-realm.json
{
  "format": "the-keep/campaign",
  "version": 1,
  "meta": {
    "title": "The Marches of Vurn",
    "blurb": "A small border realm of feuding holdfasts.",
    "playerMapAccess": false
  },
  "GROUPS": {
    "pcs":        { "label": "The Errant Company",   "kind": "pcs",     "accent": "#6b4a2b" },
    "aldermarch": { "label": "Holdfast of Aldermarch","kind": "holding", "accent": "#7a2f2f", "letter": "A" },
    "hollow":     { "label": "The Hollow Court",      "kind": "myth",    "accent": "#5a3a5a", "hidden": true }
  },
  "EDGE_TYPES": {
    "bond":  { "label": "bonded",      "color": "#2f6b4a" },
    "rival": { "label": "rival",       "color": "#a05a2c" },
    "debt":  { "label": "in debt to",  "color": "#5a3a5a" }
  },
  "npcs": [
    {
      "id": "sela",
      "name": "Sela the Wright",
      "role": "Knight-errant",
      "group": "pcs",
      "isPC": true,
      "status": "alive",
      "steed": "a grey courser",
      "facts": ["Carries her mother's smith-hammer.", "Swore an oath at the Drowned Shrine."]
    },
    {
      "id": "marl",
      "name": "Reeve Marl",
      "role": "Reeve of Aldermarch",
      "group": "aldermarch",
      "status": "alive",
      "facts": ["Keeps the holdfast's tithes and tallies."],
      "hiddenFacts": ["Skims grain to pay a debt to the Hollow Court."]
    },
    {
      "id": "envoy",
      "name": "The Pale Envoy",
      "role": "Messenger of the Hollow Court",
      "group": "hollow",
      "status": "unknown",
      "facts": [],
      "hiddenEntirely": true
    }
  ],
  "relationships": [
    { "from": "sela", "to": "marl",  "type": "bond", "visibility": "known",  "detail": "The Company guards Aldermarch's road." },
    { "from": "marl", "to": "envoy", "type": "debt", "visibility": "hidden", "detail": "Owes the Hollow Court for last winter's grain." }
  ]
}

← Back to dashboard