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.
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.
| Key | What it is |
|---|---|
meta | Title, optional blurb, and whether players may see the map. |
GROUPS | The factions, holdfasts, places and myths NPCs belong to. Keyed by id. Always include one with kind: "pcs" for the player characters. |
EDGE_TYPES | Reusable relationship categories (bonded, rival, sworn to…) with a colour. |
npcs | The people. Each has a unique id, a group, player-visible facts, and optional referee-only hiddenFacts. |
relationships | Links 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 see | Referee 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.
{
"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." }
]
}