Eposyne Director

Plots

Plots tracks the stories in your game. You author story arcs and upload them. While the player plays, Plots reads the events you send, starts and advances arcs, and tells Director what the story needs. This page covers the arc library, the running arcs, the Plots verdict in a decision, and what Plots costs.

Switch Plots on first

Plots is off by default for every account, on every plan. Switch it on under Modules at https://architect.eposyne.com/profile?section=director-keys. A switch reaches your API keys within a minute and your sign-in tokens within an hour. Until then every Plots endpoint answers 403: {"detail": "module 'plots' is not enabled for this tenant"}.

How Plots works

Plots never changes your game. It reports and suggests. Your engine stays the final authority.

The arc library

The library holds the arcs you author. When plots.sources in the policy includes authored, Plots offers them as candidates for a new arc. An account has at most 500 arcs, and one arc document is at most 262,144 bytes (256 KB).

EndpointPurpose
PUT /v2/plots/libraryStore one arc.
GET /v2/plots/libraryList the stored arcs.
DELETE /v2/plots/library/{arc_id}Remove one arc.
GET /v2/plots/runtime/arcsList the arcs running on a timeline.

Upload an arc

Request
curl -X PUT https://director.eposyne.com/v2/plots/library \
  -H "Authorization: Bearer $EPOSYNE_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @arc.json
200
{"arc_id": "arc.missing-sister", "stored": true}

One arc goes in each request. The arc_id is the id of the Arc entity. Upload an arc whose id the library already holds, and it replaces the old one. Replacing is always allowed, also when the library is full. Director stores every arc as authored, whatever its source says.

arc.json
{
  "source": "authored",
  "entities": [
    {"id": "arc.missing-sister", "entity_type": "Arc", "name": "Beatrice's missing sister",
     "data": {"description": "Beatrice's sister vanished near the docks."}},
    {"id": "beat.hear-plea", "entity_type": "Beat", "name": "Hear Beatrice's plea",
     "data": {"description": "Beatrice asks the player to look for her sister.",
              "complete_when": "The player agrees to help."}},
    {"id": "beat.search-docks", "entity_type": "Beat", "name": "Search the docks",
     "data": {"description": "The player searches the docks for clues.",
              "complete_when": "The player finds the sister's scarf."}},
    {"id": "char.beatrice", "entity_type": "Character", "name": "Beatrice",
     "data": {"game_id": "npc-beatrice", "group": "harbour"}}
  ],
  "relationships": [
    {"from_id": "beat.hear-plea", "to_id": "arc.missing-sister", "relationship_type": "children", "order": 1},
    {"from_id": "beat.search-docks", "to_id": "arc.missing-sister", "relationship_type": "children", "order": 2},
    {"from_id": "beat.hear-plea", "to_id": "beat.search-docks", "relationship_type": "beat_beat"},
    {"from_id": "beat.hear-plea", "to_id": "char.beatrice", "relationship_type": "cast"}
  ],
  "schema": {
    "Arc": [{"name": "description", "type": "text", "description": "What the arc is about"}],
    "Beat": [
      {"name": "description", "type": "text", "description": "What happens in the beat"},
      {"name": "complete_when", "type": "text", "description": "What completes the beat"}
    ],
    "Character": [{"name": "group", "type": "string", "description": "The character's group"}]
  },
  "roles": []
}

The arc format

An arc is a small graph, written in four lists:

Some data keys have a fixed meaning:

KeyOnMeaning
game_idAny entityThe id your game uses for it in events and decisions. Plots matches a cast character, or a beat's location, by game_id, then by entity id.
requires_rolesBeatThe open role slots that take part in the beat. A character bound to one counts as in the current beat.
branchesConditionalNodeThe branches of the fork. Required.
reference_idArcFor an arc built from the template catalogue: the catalogue reference id.

Fields Director reads by name, every one of them optional:

TypeFieldRead for
ArcdescriptionChoosing and filling in an arc
BeatdescriptionFilling in, {{SLOT}} tokens, relevance
Beatcomplete_whenWhen the beat is complete. Without it Plots infers completion.
CharactergroupMemory group meetings, see Memory

Plots fills in fields the schema lists that are empty (missing, null, blank, an empty list, or an empty object), with a model call. It never changes a field you wrote.

Forks and conditions

A ConditionalNode is a fork. Its data.branches is a list of branches. Each branch has a label, an order (tried in ascending order), a target_id, and a condition.

The strict rules

Director checks the whole arc before it stores anything. An arc that breaks a rule is refused with 422 and nothing is stored. The detail starts with arc bundle rejected: and lists every reason, separated by semicolons.

422
{"detail": "arc bundle rejected: the bundle has an unknown member 'colour'; the bundle needs exactly one Arc entity, found 0"}

Library limits

LimitValueWhen exceeded
Arcs in the library500409, "code": "library_full", for an arc id the library does not hold. Delete an arc first, or replace one of those it holds.
Size of one arc document262,144 bytes (256 KB)422, "code": "arc_too_large". Director reads no further than the limit.
Library writes (PUT and DELETE)600 per minute429, "code": "library_rate_exceeded", with a Retry-After header in seconds. The write is not made.
409
{"detail": "...; delete an arc first, or replace one of those it holds", "code": "library_full"}

Reading the library counts toward no limit. Library writes have a limit of their own and do not use the request limit of 60 per minute.

List and delete

Request
curl https://director.eposyne.com/v2/plots/library \
  -H "Authorization: Bearer $EPOSYNE_API_KEY"
200
{"arcs": [{"arc_id": "arc.missing-sister", "name": "Beatrice's missing sister"}]}

The list is in the order the arcs were first stored.

Request
curl -i -X DELETE https://director.eposyne.com/v2/plots/library/arc.missing-sister \
  -H "Authorization: Bearer $EPOSYNE_API_KEY"

A delete answers 204 with no body. An id the library does not hold answers 404 with a plain detail. A deleted arc is no longer a candidate for a new arc. Arcs already running keep the version they started with and run to the end, so a delete never changes a story the player is in. Upload the arc again to bring it back.

Running arcs

Read the arcs a timeline sees:

Request
curl "https://director.eposyne.com/v2/plots/runtime/arcs?world_instance_id=save-1&timeline_id=main&player_id=player-1" \
  -H "Authorization: Bearer $EPOSYNE_API_KEY"
200
{
  "world_instance_id": "save-1",
  "timeline_id": "main",
  "player_id": "player-1",
  "arcs": [
    {
      "instance_id": "inst-7f3a",
      "arc_id": "arc.missing-sister",
      "title": "Beatrice's missing sister",
      "player_id": "player-1",
      "source": "authored",
      "status": "active",
      "current_beat": {"id": "beat.search-docks", "title": "Search the docks"},
      "previous_beat": {"id": "beat.hear-plea", "title": "Hear Beatrice's plea"},
      "bindings": {
        "cast:Beatrice": {"id": "npc-beatrice", "name": "Beatrice", "fixed": true}
      },
      "started_seq": 3,
      "updated_seq": 9
    }
  ]
}
MemberMeaning
instance_idThe running arc. A verdict names it as arc_instance_id.
arc_id, titleThe Arc entity's id and name.
player_idThe player the arc runs for.
sourceauthored (from your library) or emergent (from the catalogue, or invented).
statusactive or completed. The read lists both. Filter on your side.
current_beat, previous_beatThe beat now current and the one before, with the title from the arc, or null. A completed arc has no current beat.
bindingsRole name to the character bound to it. fixed is true for a character you cast in the arc, false for one Plots bound to an open role.
started_seq, updated_seqThe stream positions at which the arc started and last changed.

The Plots verdict in a decision

Name a stream position on POST /v2/decide and the reply carries what Plots did for the player since their previous decision.

Request
curl -X POST "https://director.eposyne.com/v2/decide?world=save-1&timeline=main&after_seq=44" \
  -H "Authorization: Bearer $EPOSYNE_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @request.json

The stream position

Whose verdicts

A decision reports the verdicts of one player. With a stream position, add the query parameter player_id to name whose:

Request
curl -X POST "https://director.eposyne.com/v2/decide?world=save-1&timeline=main&after_seq=60&player_id=player-2" \
  -H "Authorization: Bearer $EPOSYNE_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @request.json

The verdict

JSON
{
  "schema": "eposyne.director.decision-response/v2",
  "changes": [
    {"name": "hale_mood", "value": "pleading", "rationale": "Hale needs the player's silence about the dark lamp."}
  ],
  "plots": {
    "steering": "subtle",
    "verdicts": [
      {
        "kind": "started",
        "seq": 44,
        "arc_instance_id": "inst-3c9e",
        "arc_name": "The Lighthouse Debt",
        "to_beat": "The dark lamp",
        "reason": "The player saw the lighthouse lamp go dark and is asking the keeper why."
      }
    ]
  }
}

seq is a number. verdicts is always an array.

kindMeaningMembers
startedA new arc started.arc_instance_id, arc_name, to_beat (the first beat), reason (the premise)
advancedThe current beat completed and the arc moved on, through any fork.arc_instance_id, arc_name, from_beat, to_beat
completedThe last beat completed and the arc ended.arc_instance_id, arc_name, from_beat
fitsThe event belongs to a running arc without completing its beat, or the beat is complete and no branch applies yet.arc_instance_id, arc_name
refusedA possible new arc was not started, by your policy's limits or by the gate. The reply steers the player away from it, as steering says.reason. A refused verdict names no arc.

The arcs that matter for the deciding character also reach the decision as direction, filtered by plots.relevance. That text is for Director. It is never knowledge the character holds, and it is not part of the response.

What Plots costs

The counts and the settings that change them are on Policy. What credits cover is on Billing and limits.