The FableDraft world file
FableDraft imports a whole world from one JSON file. The same format comes out when you export a world, so a file can go out and back in. You can also write one by hand, generate it with a script, or ask an AI assistant to turn your notes into one.
- JSON Schema:
/schemas/world-1.json - A complete example to download:
/examples/example-world.json
Only two things are required: the world name, and a type and name on
each entity. Everything else is optional.
The smallest file
{
"name": "Tiny",
"entities": {
"mira": {"type": "character", "name": "Mira", "fields": {"homeLocationId": "port"}},
"port": {"type": "location", "name": "Port", "body": "Home of [[id:mira]]."}
},
"links": [{"fromId": "mira", "toId": "port", "type": "located_in"}]
}
This makes a world called Tiny with a character, a town, and a link between
them. mira and port are ids. You make them up; they only matter when
something else needs to point at that entity.
A fuller example
This is the downloadable example file. It has two characters, a town, a faction, a quest with one step, a link, a journal entry and a calendar era.
{
"$schema": "https://fabledraft.com/schemas/world-1.json",
"name": "The Saltglass Reach",
"description": "A coast where the sea froze into glass. Smugglers light paths across it.",
"calendar": {
"dateOrder": "mdy",
"yearLabel": "AR",
"eras": [{"name": "The Glass Years", "start": 1, "end": null, "color": "#3b6ea5"}]
},
"entities": [
{"id": "gullhaven", "type": "location", "name": "Gullhaven",
"summary": "A harbour town on the edge of the glass.",
"fields": {"kind": "town", "founded": "-120"}},
{"id": "guild", "type": "faction", "name": "The Lantern Guild",
"description": "Smugglers who mark the safe paths across the ice.",
"fields": {"leaderId": "Vessa Crane", "baseLocationId": "Gullhaven"}},
{"id": "mira", "type": "character", "name": "Mira Saltwhistle", "tags": ["party"],
"summary": "A lamplighter who owes the guild money.",
"fields": {"role": "pc", "born": "04-17-38", "homeLocationId": "gullhaven", "factionIds": ["guild"]}},
{"id": "vessa", "type": "character", "name": "Vessa Crane",
"body": "Captain of [[The Lantern Guild]]. Trusts [[id:mira|Mira]] more than she should.",
"fields": {"role": "ally", "homeLocationId": "gullhaven", "factionIds": ["guild"]}},
{"id": "lights", "type": "quest", "name": "Lights on the Glass", "status": "active",
"fields": {"giverId": "vessa", "difficulty": "easy", "locationIds": ["gullhaven"],
"objectives": [{"id": "s1", "text": "Relight the north lantern", "done": false, "action": "go", "locationId": "gullhaven"}]}}
],
"links": [
{"fromId": "mira", "toId": "Vessa Crane", "type": "friend", "label": "old cellmates"}
],
"journal": [
{"date": "03-02-61", "title": "Session 1", "entityIds": ["mira", "lights"],
"body": "[[Mira Saltwhistle]] took the lantern job from [[Vessa Crane]]."}
]
}
Some things to notice:
- Entities are a list here. They can also be an object keyed by id, as in the smallest file.
- References use an id (
"vessa") or a name ("Vessa Crane"). Both work. - The faction uses
descriptionfor its main text. That is the same asbody. "$schema"is optional. It lets editors like VS Code check the file as you type.
Ids and references
An id is any short string you choose: mira, loc-1, 42. FableDraft keeps
an id when it is 8 to 12 letters, digits, _ or -. Any other id is replaced
with a new one, and every place that pointed at it is updated to match. You
never need to care what the final ids are. Give every entity a different id.
A reference is any key that points at an entity:
parentIdon an entityfromIdandtoIdon a link- any field whose key ends in
Id(one entity) orIds(a list), including inside quest steps, rewards and talks. Three keys are not entity references and are kept as written:stepId(a quest step),switchId(a world switch) andtalkId(a talk) entityIdson a journal entryentityIdandunderIdon a map marker, andlocationIdon a map
A reference can hold an entity's id or its name. A name matches without caring about upper or lower case. If two entities share a name, the first one in the file wins. Use ids when names repeat.
Top level
| Key | Kind | Notes |
|---|---|---|
name |
text | Required. The world's name. |
description |
text | Shown on the world's home page. Markdown and wikilinks work. |
style |
text | Notes on look and tone for the AI tools. Up to 12,000 characters. |
portraitStyle |
text | How character portraits look, e.g. "cartoonish, bold outlines". The first choice when you make a portrait. Up to 500 characters; longer text is cut. |
entities |
object or list | See Entities. |
links |
list | See Links. |
journal |
list | See Journal. |
maps |
list | See Maps. |
calendar |
object | See Calendar and dates. |
settings |
object | See Settings. |
nameCultures, nameLists |
list | See Name lists. |
$schema |
text | Ignored on import. |
The whole object may also sit inside a "world" key: {"world": {...}}.
Exports from older versions look like that.
Entities
Every entity has a type. The eight types are arc, quest, character,
location, faction, item, event and note.
| Key | Kind | Notes |
|---|---|---|
type |
one of the eight types | Required. |
name |
text | Required. |
id |
text | Optional. Needed only if something points at this entity. |
summary |
text | A line or two. Markdown and wikilinks work. If it is empty, tagline is used. |
body |
text | The main text. Markdown and wikilinks work. If it is empty, the first non-empty one of description, text and content is used. |
status |
text | One of the world's statuses. Default idea. A status the world does not have yet is added to its list. |
tags |
list of text | |
parentId |
reference | An arc can sit under an arc. A quest can sit under an arc or a quest. A location can sit under a location. Other types have no parent. |
order |
number | Position among siblings. |
fields |
object | The type's own fields, below. |
portraitData |
image data URL | Characters only. See Images. |
created, updated |
timestamp | For example 2026-10-01T12:00:00Z. Default: the time of import. |
The default statuses are idea, planned, active, completed, failed,
dead and retired.
Fields by type
Kinds:
- text: a string.
- choice: one of the listed values. Another word is kept as written, with a warning.
- date: an in-world date, see Calendar and dates.
- ref / refs: one reference, or a list of references, to the listed type.
- yes/no:
trueorfalse.1,0and the strings"yes","no","on","off","true"and"false"work too. - number: a number, or a string that holds one.
Every type can also have parentRelation (text): what the line to the parent
says in the graph, like "part of". Empty shows "parent".
| Type | Key | Kind | Values or target |
|---|---|---|---|
| arc | startDate, endDate |
date | |
| arc | kind |
choice | main, sub |
| arc | theme |
text | |
| arc | hook, climax, resolution |
text | |
| quest | giverId |
ref | character or item (an item such as a letter the player reads) |
| quest | turnInId |
ref | character. Empty: the player turns the quest in to the giver when the giver is a character, and the quest ends at its last step when the giver is an item. |
| quest | difficulty |
choice | easy, normal, hard, deadly |
| quest | timeLimit |
text | |
| quest | optional |
yes/no | |
| quest | locationIds |
refs | location |
| quest | itemIds |
refs | item |
| quest | prerequisiteIds |
refs | quest |
| quest | hook, ending, failure |
text | |
| quest | journalText |
text | What the player reads in the journal, up to 2000 characters. A longer text is cut, with a warning. |
| quest | objectives |
list of steps | see below |
| quest | rewards |
list of rewards | see below |
| character | born, died |
date | |
| character | role |
choice | pc, npc, villain, ally, neutral |
| character | race, class, age, pronouns |
text | |
| character | homeLocationId |
ref | location |
| character | factionIds |
refs | faction |
| character | appearance, personality, motivation, secrets |
text | |
| character | statblock |
text | shown in a fixed-width font |
| character | portraitUrl |
text | a link to an image |
| character | talks |
list of talks | see Talks |
| location | founded |
date | |
| location | kind |
choice | continent, region, city, town, dungeon, building, wilderness, other |
| location | population, government |
text | |
| faction | founded, dissolved |
date | |
| faction | leaderId |
ref | character |
| faction | baseLocationId |
ref | location |
| faction | alignment, goals |
text | |
| item | created |
date | |
| item | category |
text | one of the world's item categories |
| item | kind |
choice | weapon, armor, artifact, consumable, key, treasure, other |
| item | rarity |
choice | common, uncommon, rare, very_rare, legendary |
| item | value |
number | in gold |
| item | ownerId |
ref | character or faction |
| event | date, endDate |
date | |
| event | when |
text | a free-text time, for example "the winter after the war" |
| event | arcId |
ref | arc |
| event | locationId |
ref | location |
| note | no fields |
A quest step (objectives) is an object:
| Key | Kind | Notes |
|---|---|---|
id |
text | Any short string. Give each step one. |
text |
text | What happens. |
done |
yes/no | |
locationId |
ref | location |
itemIds |
refs | item |
characterIds |
refs | character |
talkId |
text | The id of a talk (on any character). Reaching the end of that talk finishes the step. An id no talk in the file has is dropped, with a warning. |
action |
choice | read, collect, kill, talk, go, use, other. Missing means a story-only step. Another word is kept as written, with a warning. |
count |
number | A whole number from 1 to 9999: how many times, for collect and kill. Missing means 1. Any other value is dropped, with a warning. |
targetText |
text | Up to 120 characters. For collect and kill: a target with no entry, like pig. |
journalText |
text | Up to 2000 characters. What the game logs when the step finishes. Missing means nothing is logged. |
count and targetText stay only under collect, kill or a word off the list. Under any other action, or none, they are removed with no warning. Text over its limit is cut, with a warning.
The target of a step, by action:
| Action | Target |
|---|---|
read, use |
itemIds |
collect |
itemIds, else targetText |
kill |
characterIds, else targetText |
talk |
characterIds, plus talkId if one set talk is needed |
go |
locationId |
other |
text only |
If you read this data to build a game:
- When a list holds more than one entry, the first is the target and the rest are context.
countapplies to that one target. locationIdon any action butgosays where it happens. It is context, not a target.- An action with no target is valid. Treat it like
other. doneis the author's own progress tick. A game ignores it.
A quest reward (rewards) is an object:
| Key | Kind | Notes |
|---|---|---|
id |
text | Any short string. |
kind |
choice | item, xp, gold, reputation, other |
itemId |
ref | an item, when kind is item |
amount |
number | |
text |
text |
Any other key in fields is kept as written, with a warning.
Talks
A talk is one conversation between a character and the player. A character's
talks is a list of talks. The game plays the first talk in the list whose
conditions pass, so put the most specific talks first.
"talks": [{
"id": "t-k3j9x2",
"title": "First meeting",
"conditions": [{"kind": "quest", "questId": "lantern", "state": "not_started"}],
"lines": [
{"id": "l1", "speaker": "character", "text": "You look lost, traveller.",
"conditions": [], "effects": [], "next": null,
"choices": [
{"id": "c1", "text": "I'm looking for the lantern keeper.",
"conditions": [], "effects": [{"kind": "start_quest", "questId": "lantern"}], "next": "l2"},
{"id": "c2", "text": "Never mind.", "conditions": [], "effects": [], "next": "end"}
]},
{"id": "l2", "speaker": "character", "text": "Follow the coast north.", "next": "end"}
]
}]
A talk:
| Key | Kind | Notes |
|---|---|---|
id |
text | Unique in the world, and not the id of an entity. FableDraft makes them as t- and 6 letters or digits. A missing id is made; one already used by another talk or an entity gets a new id, with a warning (steps pointing at it keep pointing at the first talk). |
title |
text | Up to 120 characters. |
conditions |
list of conditions | The talk can play only when all of them pass. |
lines |
list of lines | In order. The first line is the start. Up to 200. |
A line:
| Key | Kind | Notes |
|---|---|---|
id |
text | Unique in its talk (lines and choices share the ids). The editor uses l1, l2... |
speaker |
character or player |
Default character. Another word becomes character, with a warning. |
text |
text | Up to 2000 characters. Longer text is cut, with a warning. |
conditions |
list of conditions | The line is skipped when one fails. |
effects |
list of effects | They run when the line is shown. |
next |
null, "end" or a line id |
null goes to the line below (the talk ends after the last line). A line with choices ignores next: the picked choice decides. Any other value becomes null, with a warning. |
choices |
list of choices | What the player can answer. Up to 8. |
A choice has id, text (up to 2000 characters), conditions (the choice is
hidden when one fails), effects (they run when it is picked) and next (a
line id, "end", or null for the line below).
All conditions in one list must pass. Each list holds up to 10 conditions or
effects. Longer lists are cut, with a warning. A condition or effect is an
object with a kind:
Condition kind |
Keys | Passes when |
|---|---|---|
quest |
questId, state: not_started, active, done or failed |
the player's quest is in that state |
step |
questId, stepId, state: done or not_done |
that step of the quest is (not) done |
item |
itemId, has: yes/no |
the player holds (or does not hold) the item |
switch |
switchId, on: yes/no |
the world switch is on (or off) |
Effect kind |
Keys | Does |
|---|---|---|
start_quest |
questId |
starts the quest |
finish_step |
questId, stepId |
marks the step done |
finish_quest |
questId |
finishes the quest |
set_switch |
switchId, on: yes/no |
turns the switch on or off |
give_item, take_item |
itemId |
gives the item to the player, or takes it |
questId and itemId are references to a quest and an item (an id or a
name). stepId is the id of a step in that quest's objectives. switchId
is the id of a switch in Settings. A missing has or on
means true.
On import, a condition or effect whose reference cannot be found, points at
the wrong type, or names an unknown switch is dropped, with a warning. A
condition or effect with a kind FableDraft does not know is kept as written,
with a warning; it is dropped (with a notice) the next time the character is
saved in FableDraft.
Links
A link is a named relation between two entities. It shows on both entity pages and in the graph.
| Key | Kind | Notes |
|---|---|---|
fromId |
reference | Required. |
toId |
reference | Required. |
type |
text | Usually one of the world's relation types. Default other. |
label |
text | A short note shown on the link, like "captain". |
notes |
text | Longer notes. Wikilinks work. |
id, created |
Optional. |
The default relation types are ally, enemy, rival, family, friend,
lover, mentor, member_of, leads, located_in, owns, knows,
serves, created and other.
Journal
Session notes.
| Key | Kind | Notes |
|---|---|---|
date |
text | Free text. Usually the real date of the session. |
title |
text | |
body |
text | Markdown and wikilinks work. |
entityIds |
list of references | The entities this entry is about. |
id, created, updated |
Optional. |
Maps
| Key | Kind | Notes |
|---|---|---|
name |
text | Default Map. |
imageData |
image data URL | The map picture. See Images. |
locationId |
reference | The location this map shows. Ignored if it is not a location. |
markers |
list | Pins on the map. |
id, created, updated |
Optional. |
A marker:
| Key | Kind | Notes |
|---|---|---|
entityId |
reference | Required. One entity can have one marker per map. A second marker for it is dropped, with a warning. |
x |
number 0 to 1 | Required. 0 is the left edge, 1 the right edge. |
y |
number 0 to 1 | Required. 0 is the top edge, 1 the bottom edge. |
underId |
reference | A location the marker sits inside. |
id |
text | Optional. |
Positions are fractions of the image, so they stay put when the image is resized. Numbers outside 0 to 1 are pulled back to the nearest edge.
Calendar and dates
| Key | Kind | Notes |
|---|---|---|
dateOrder |
"mdy" |
Put this in. It tells FableDraft your dates are month first. |
yearLabel |
text | Shown after years, like AR or DR. |
monthNames |
list of text | Up to 12 names. |
daysPerMonth |
whole number or null |
Days in every month, 1 to 31. Leave it out or null for 31. |
weekDays |
list of text | Up to 14 week day names, in order. Day 1 of month 1 of year 1 is the first one. |
today |
date text | The in-world date it is now, like 04-17-1250. A character's age is counted from born up to died, or up to this day; a counted age replaces the one in the file. |
eras |
list | Named spans of years. |
An era:
| Key | Kind | Notes |
|---|---|---|
name |
text | Required. |
start |
whole number | Required. The first year. |
end |
whole number or null |
The last year. null means it has not ended. Must not be before start. |
color |
text | Like #3b6ea5. |
summary |
text | |
id |
text | Optional. |
An in-world date is one of:
MM-DD-YYYY, for example04-17-1203MM-YYYY, for example04-1203YYYY, for example1203
The year always comes last. Use - or / between the parts. Years can be
negative (-300) and up to six digits. Months go from 1 to 12 and days from 1
to 31. Leading zeros are optional.
Old exports wrote dates year first (1203-4-17). Without
"dateOrder": "mdy", a date that cannot be read month first but looks year
first is turned around (1203-4-17 becomes 04-17-1203), with a warning. An
old export (one with a top-level created key and no dateOrder) has all its
dates read year first. With "dateOrder": "mdy", dates are never turned
around.
Settings
| Key | Kind | Notes |
|---|---|---|
statuses |
list of text | Replaces the default statuses. Must not be empty. |
relationTypes |
list of text | Replaces the default link types. Must not be empty. |
itemCategories |
list of text | Replaces the default item categories (weapons, clothing, books). |
switches |
list | On/off flags of the world that talks test and set. Up to 200. |
A switch:
| Key | Kind | Notes |
|---|---|---|
id |
text | Kept as written when it is 1 to 64 letters, digits, _ or - and not used twice; otherwise a new id. FableDraft makes them as sw- and 6 letters or digits. |
name |
text | Required. Up to 120 characters. |
on |
yes/no | The value it starts with. Default false. |
Leave settings out to keep the defaults.
Name lists
Name lists hold names for the name generator. Most files can leave them out.
nameCultures: a list of{id, name, style, order}.styleis a short note on how names in this culture sound.nameLists: a list of{id, title, kind, culture, structure, style, names, parts, isPublic, order}.kindis one ofcharacter,place,region,town,faction,item,other.structureissingleorgiven_family.cultureis the id of an entry innameCultures.namesis a list of strings or{name, note, starred}objects, up to 500.partsis{prefixes: [...], suffixes: [...]}.
Wikilinks
Any text that allows Markdown can link to an entity:
| You write | It shows |
|---|---|
[[Vessa Crane]] |
a link to the entity named Vessa Crane |
[[Vessa Crane|the captain]] |
the same link, labelled "the captain" |
[[id:vessa]] |
a link to the entity with id vessa, labelled with its name |
[[id:vessa|the captain]] |
the same, with your own label |
Name links match without caring about case. A name with no matching entity
shows as a "click to create" link. [[id:...]] links are updated on import
when an id is replaced.
Images
Images are base64 data URLs, for example data:image/png;base64,iVBORw0....
PNG, JPEG, GIF and WebP work. An image over the size limit (10 MB by
default) is re-encoded as WebP on import, so it usually still fits. A character portrait goes in portraitData on
the entity. Images on any other type are not stored.
Limits
- The file can be up to 64 MB by default. The server admin can change this.
- One import makes one new world. It never changes an existing world.
What stops an import, and what only warns
Nothing is saved until the whole file has been checked. Either the whole world is imported or none of it is.
These stop the import, with a message naming the key:
- The file is not valid JSON, or the top level is not an object.
- The world has no
name. - An entity has no
name, or itstypeis not one of the eight types. - A key has the wrong kind of value:
fieldsis not an object,orderis not a number,tagsis not a list of strings,summaryorbodyis not a string. parentIdchains loop back on themselves.- A marker has no
xory. - An image is not PNG, JPEG, GIF or WebP, or is too large even after shrinking.
- A calendar era has no
name, astartthat is not a whole number, anendbefore itsstart, or acolorthat is not like#a1b2c3. daysPerMonthis not a whole number from 1 to 31 (ornull), orweekDayshas more than 14 names.- The calendar's
todayis not a date. monthNameshas more than 12 names, orsettings.statusesorsettings.relationTypesis an empty list.settings.switchesis not a list of objects, has more than 200 switches, or a switch has noname(or one over 120 characters).
These import, with a warning in the import report:
- An unknown key inside
fieldsis kept as written. - A choice not in the list, or a date in the wrong form, is kept as written.
- A list or object where a single value belongs is dropped. So is a number field that holds no number, and a yes/no field that is not yes or no.
- A second marker for the same entity on one map is dropped.
- A reference that matches no id and no name is dropped. A link with such a reference is dropped as a whole, and so is a map marker.
- A status the world does not have is added to the world's statuses.
- An entity id used twice: the later entity gets a new id. References to that id point at the first entity.
- A year-first date in a file without
dateOrderis turned around. - Talks: what breaks the rules in Talks is repaired or dropped
(text cut to its limit, a bad
nextorspeakerreset, a clashing talk id replaced, extra talks, lines, choices or rules cut). Unknown condition and effect kinds are kept.
These import without a warning:
- Unknown keys outside
fieldsare ignored. - A parent of the wrong type is removed, so the entity sits at the top level.
- Marker positions outside 0 to 1 are pulled back to the nearest edge.
- Ids that are not 8 to 12 letters, digits,
_or-are replaced.
About the schema
world-1.json is a JSON Schema (draft 2020-12). Its $id is
https://fabledraft.com/schemas/world-1.json, and every FableDraft site
serves the same file at /schemas/world-1.json.
The schema allows extra keys everywhere, like the importer. It is a little stricter than the importer in a few places. The schema lists the usual choices; the importer accepts other words and warns. The schema also flags a badly formed date, a yes/no or number written as a string, and a marker position outside 0 to 1, where the importer keeps, converts or fixes the value and carries on. A file that passes the schema imports without those warnings.
The schema cannot check that a reference points at a real entity. The importer does that.