Skip to main content

PajamaScript: Interactive Story Format Specification

Version: 1.1
Last Updated: 2025-05-11


πŸ“š Overview​

PajamaScript is a structured, JSON-based storytelling format for defining interactive experiences in Pajama. It organizes a story into a set of pages. Stories can flow linearly or branch via choice pages, enabling interactive narrative structure.

It is designed to be both LLM-friendly and human-authorable.


πŸ“¦ Story-Level Schema​

{
"schema_version": int,
"id": string,
"title": string,
"start_page_id": string,
"pages": [string]
}

Each story defines its entry point (start_page_id) and a list of pages.


πŸ“„ StoryPage Object​

A StoryPage represents a single page of the story. All pages share the same base schema:

{
"id": "<uuid>",
"type": "title | text | choice | ending",
"image_url": "castle.jpg" | null,
"tools": [Tool]
}

Args​

  • id (string) β€” An identifier for the page, must be unique only for the current story.
  • type (string) β€” Determines the type of content displayed on the page:
    • "title" β€” A page that presents the title page of the story.
    • "text" β€” A page that presents narrative content line-by-line.
    • "choice" β€” A page that presents a set of choices that lead to different outcomes
    • "ending" β€” A page that presents an ending message.
  • image_url (string, optional) β€” An image associated with this page. Can be null or omitted if no image is available.
  • tools (list[Tool]) β€” Optional tools to activate on this page.

Title​

The first page of a story.

{
"id": "<uuid>",
"type": "text",
"header": "Little Red Riding Hood",
"subheader": "By R.L. Stein" | null,
"next_page_id": "character_intro_page",
"tools": [Tool],
}

Args​

  • header (string) β€” A header line of text
  • subheader (string or null, optional) β€” A subheader line of text
  • next_page_id (string) - Specifies the next page after the title page.

Text​

Used for displaying one ore more lines of narrative content.

{
"id": "<uuid>",
"type": "text",
"text_lines": ["Once upon a time", "in a land far, far away"],
"text_positioning": "separate",
"text_multiline_entry_strategy": "all",
"text_newline_delay_ms": -1,
"next_page_id": "castle_intro_page" | null,
"tools": [Tool],
}

Args​

  • text_lines (list[string]) β€” Story text to display, line-by-line.
  • text_positioning (string) β€” Where the text appears on screen:
    • "separate" β€” A split-screen UI with text below any available image and video feeds.
    • "overlay" β€” Text is positioned on top of any available image.
  • text_multiline_entry_strategy (string) β€”
    • "all" β€” All lines appear at once when the page loads.
    • "one_by_one_replace" β€” Lines appear one at a time, replacing the previous lines.
    • "one_by_one_append" β€” Lines appear one at a time, appended below previous lines.
  • text_newline_delay_ms (int) β€” Delay (in milliseconds) between displaying lines when using a one-by-one text_multiline_entry_strategy. If -1, waits for the user to manually tap to show the next line of text.
  • next_page_id (string or null, optional) - Specifies the next page after this page. If null or omitted, the story ends.

Choice​

Used to branch the story based on a user decision.

{
"id": "<uuid>",
"type": "choice",
"header": "Who will you choose to help?" | null,
"choices": [
{ "label": "Fiona", "next_page_id": "fiona_castle" },
{ "label": "Shrek", "next_page_id": "shrek_swamp" },
{ "label": "Donkey", "next_page_id": "donkey_waffle_house" },
],
"tools": [Tool],
}

Args​

  • header (string, optional) β€” A header to display above the choices.
  • choices (list[object]) β€” List of available choices:
    • "label" β€” A human-readable text label for this choice.
    • "next_page_id" β€” The page to navigate to if this choice is selected.

Ending​

The last page of a story. The reader client will usually display buttons to re-read or exit the story.

{
"id": "<uuid>",
"type": "text",
"header": ["The End"],
"tools": [Tool],
}

Args​

  • header (list[string]) β€” A header line of text

πŸ›  Tool Object​

A Tool adds interactivity to a page such as visual or audio effects. All tools follow the same schema:

{
"name": "<tool_name>",
"version": 1,
"scope": "page | story",
"args": { ... }
}

Args​

  • name (string) β€” A unique identifier for the tool.
  • version (int) β€” The tool schema version. Increment this if the tool’s interface changes.
  • scope (string) β€” Determines the tool's lifespan:
    • "page" β€” The tool is active only for the current page.
    • "story" β€” The tool remains active for the remainder of the story or until explicitly ended on a later page.
  • args (object) β€” Tool-specific arguments.

Notes​

  • Multiple tools can be used per page.

🎭 Supported Tools​

ar_mask​

Applies an augmented reality mask to one or more participants.

{
"name": "ar_mask",
"version": 1,
"scope": "page | story",
"args": {
"target": "reader | listeners | all",
"mask_id": "volcano_hat"
}
}

Args​

  • target (string) β€” Specifies which participants receive the mask.
  • "reader" β€” The participant reading the story who has control over page flips.
  • "listeners" β€” All participants who are not the reader.
  • "all" β€” All participants.
  • mask_id (string) β€” A unique identifier for the AR mask asset to render.

Notes​

  • Multiple AR masks can be enabled simultaneously, but each one must use a different target.

sound_effect​

Plays a short sound effect triggered by a page event or user interaction.

{
"name": "sound_effect",
"version": 1,
"scope": "page | story",
"args": {
"sound_url": "drum_roll",
"trigger": "page_enter | first_page_tap | every_page_tap",
"broadcast_taps": true,
"delay_ms": 500
}
}

Args​

  • sound_url (string) β€” URL of the sound effect to play. Examples: "magic_twinkle", "frog_ribbit", "drum_roll".
  • trigger (string) β€” When the sound should play:
    • "page_enter" β€” Immediately when the page finishes loading.
    • "first_page_tap" β€” The first time the user taps the page.
    • "every_page_tap" β€” Every time the user taps the page.
  • broadcast_taps (boolean) β€” If true, the sound plays for everyone in the call when tapped. If false, the sound is only played locally for the tapper. Ignored if trigger is not a tap.
  • delay_ms (int) β€” Delay (in milliseconds) after trigger before playing the sound effect.

Notes​

  • Only one sound effect may be enabled for a page. If scope is "page", it overrides any existing sound effects.

background_music​

Plays a background music track.

{
"name": "background_music",
"version": 1,
"scope": "page | story",
"args": {
"sound_url": "general_adventure",
"loop": true
}
}

Args​

  • sound_url (string) β€” URL of the background music file. Examples: "spooky_castle", "mysterious_corridor", "general_adventure".
  • loop (boolean) β€” If true, the music loops continuously for the duration of the scope. If false, playback ends when the track finishes.

Notes​

  • Only one background music may be enabled for a page. If scope is "page", it overrides any existing background music.

πŸ“˜ Example PajamaScript​

The payload when fetching a story from /story/luna-moon-adventure-v1.1:

{
"schema_version": 1,
"id": "luna-moon-adventure-v1.1",
"title": "Luna's Magical Moon Adventure",
"start_page_id": "intro_page-1",
"page_ids" [
]
}

The payload when fetching pages from /story/luna-moon-adventure-v1.1/page:

[
{
"id": "intro-page-1",
"type": "text",
"image_url": "assets/images/luna_looking_at_moon.png",
"text_lines": [
"Luna loved the moon.",
"Tonight, it seemed to shine just for her!"
],
"text_positioning": "separate",
"text_multiline_entry_strategy": "one_by_one_append",
"text_newline_delay_ms": 2000,
"tools": [
{
"name": "background_music",
"version": 1,
"scope": "story",
"args": {
"sound_url": "assets/music/dreamy_night_theme.mp3",
"loop": true
}
}
],
"next_page_id": "intro-page-2",
},
{
"id": "intro-page-2",
"type": "text",
"image_url": "assets/images/moonbeam_window.png",
"text_lines": [
"Suddenly, a moonbeam shimmered through her window...",
"...and a sparkling ladder appeared!"
],
"text_positioning": "overlay",
"text_multiline_entry_strategy": "one_by_one_replace",
"text_newline_delay_ms": 2500,
"tools": [
{
"name": "sound_effect",
"version": 1,
"scope": "page",
"args": {
"sound_url": "assets/sfx/magic_twinkle_long.mp3",
"trigger": "page_enter"
}
}
],
"next_page_id": "moon-page-1"
},
{
"id": "moon-page-1",
"type": "text",
"image_url": "assets/images/luna_on_moon.png",
"text_lines": [
"Whoosh! Luna found herself on the moon!",
"It was bouncy and made of cheese (or so she thought)!"
],
"text_positioning": "separate",
"text_multiline_entry_strategy": "all",
"tools": [
{
"name": "ar_mask",
"version": 1,
"scope": "page",
"args": {
"target": "reader",
"mask_id": "astronaut_helmet_simple"
}
},
{
"name": "sound_effect",
"version": 1,
"scope": "page",
"args": {
"sound_url": "assets/sfx/boing.mp3",
"trigger": "first_page_tap",
"broadcast_taps": false // Only local boing for the tapper
}
}
]
"next_page_id": "moon-page-2"
},
{
"id": "moon-page-2",
"type": "choice",
"image_url": "assets/images/moon_critters_choice.png",
"choice_header": "She saw two paths. Which way should she explore?",
"choices": [
{ "label": "The Sparkly Crystal Cave", "next_page_id": "cave-page-1" },
{ "label": "The Giggling Moon Bunnies", "next_page_id": "bunny-page-1" }
],
"tools": [] // Story-scoped music "dreamy_night_theme" continues
},
{
"id": "cave-page-1",
"type": "text",
"image_url": "assets/images/crystal_cave_bright.png",
"text_lines": ["The cave was full of sparkling crystals!", "They hummed with a soft light."],
"text_positioning": "overlay",
"text_multiline_entry_strategy": "one_by_one_append",
"text_newline_delay_ms": -1,
"tools": [
{ // Page-scoped music overrides the story-scoped music
"name": "background_music",
"version": 1,
"scope": "page",
"args": { "sound_url": "assets/music/crystal_cave_shimmer.mp3", "loop": true }
}
],
"next_page_id": "conclusion-page-1"
},
{
"id": "bunny-page-1",
"type": "text",
"image_url": "assets/images/moon_bunnies_playing_luna.png",
"text_lines": ["The moon bunnies were so fluffy and playful!", "They shared their moon carrots with Luna."],
"text_positioning": "separate",
"text_multiline_entry_strategy": "all",
"tools": [
// "dreamy_night_theme" (story-scoped) continues here as no page-scoped music overrides it.
{
"name": "sound_effect",
"version": 1,
"scope": "page",
"args": { "sound_url": "assets/sfx/bunny_giggle_soft.mp3", "trigger": "page_enter", "broadcast": true }
}
],
"next_page_id": "conclusion-page-1"
},
{
"id": "conclusion-page-1",
"type": "text",
"image_url": "assets/images/luna_waving_goodbye_on_moon.png",
"text_lines": [
"What an amazing adventure!",
"Luna knew she'd never forget her trip to the moon."
],
"text_positioning": "separate",
"text_multiline_entry_strategy": "one_by_one_append",
"text_newline_delay_ms": 3000,
"tools": [
{ // Stop the story-scoped "dreamy_night_theme"
"name": "background_music",
"version": 1,
"scope": "page", // Stop it just for this page
"args": { "sound_url": "none" } // Special value to stop music
},
{
"name": "sound_effect",
"version": 1,
"scope": "page",
"args": {
"sound_url": "assets/sfx/gentle_chime_end.mp3",
"trigger": "page_enter",
"delay_ms": 500
}
}
]
// next_page_id is omitted so this signifies the END OF THE STORY.
}
]