The scripting language behind every Storyomi story.
A complete OmiScript chapter. It shows backgrounds, character dialogue, choices, variables, conditionals, jumps, and endings all working together.
Register before you reference. The engine validates your script before running it. Characters, backgrounds, variables, and chat IDs must all be set up in Creator Studio first — then you reference them by name in your script. This prevents runtime errors. The example below uses stranger, city_night, and $met_stranger — you'd create each of those in Studio before this script will pass validation.
## The Message// Register 'stranger' in Studio → Characters, 'city_night' in Assets → Backgrounds,// and '$met_stranger' (boolean) in Studio → Variables before running this script. @bg city_night // Narration — no speaker, shown as prose overlay"It was 2 AM when the phone buzzed." // Dialogue — speaker must be registered in Studio → Charactersplayer: "Who texts at this hour?"stranger: "Turn around." // Choice gate — player picks one branch; engine waits here? "What do you do?" - "Turn around slowly" player: "My hands are shaking." stranger: "Smart choice." // Session variable assignment — persisted with save slots $met_stranger = true -> The Reveal - "Ignore it" player: "Probably spam." "She put the phone down. Big mistake." -> Bad End ## The Reveal@bg alley_night "The figure stepped into the light." // Conditional — branches on a session variable value? if $met_stranger: stranger: "I knew you'd come."? else: stranger: "You shouldn't be here." stranger: "We need to talk. Not here." // @end — chapter complete, engine loads the next published chapter@end ## Bad End@bg city_night "Three hours later, she was gone." // @fin ends the entire story, not just the chapter@fin
Every construct in OmiScript is introduced by a single character. Learn the sigils and you know the whole language.
| Sigil | What it does | Example |
|---|---|---|
$varName = expr | Assign a session variable — saved with save slots, used for all story branching | $score = 0 |
$varName += expr | Add to a session variable (also -=, *=, /=) | $score += 10 |
$$varName = expr | Assign a persistent variable — survives across all playthroughs, never rolled back | $$saw_ending_b = true |
$$varName += expr | Add to a persistent variable (also -=, *=, /=) | $$total_score += 5 |
## Scene Name | Starts a new named scene — used as a jump target | ## The Rooftop |
"text" | Narration — no speaker, prose overlay | "The rain fell harder." |
speaker: "text" | Character dialogue | sarah: "I'm scared." |
speaker (expression): "text" | Dialogue with sprite expression | sarah (worried): "I can't." |
? "Label" | Choice gate — player picks a branch | ? "What will you say?" |
- "Option" | Choice option (2-space indent) | - "Run" |
-> Name | Jump to a named scene | -> The Roof |
@end | End of chapter — engine loads the next published chapter | @end |
@fin | End of story — game is completely over | @fin |
@app appId | Switch to a registered SimOS app | @app chatter |
@sprite slot charId | Stage a character sprite in a slot (left / center / right) | @sprite left sarah |
@sprite slot clear | Remove a sprite from a slot | @sprite left clear |
speaker (): "text" | Auto-stage default sprite and speak | sarah (): "Watch out!" |
speaker (expression): "text" | Auto-stage a named expression sprite | sarah (worried): "I can't." |
@bg filename | Set background image | @bg city_night |
@cg filename | Set full-screen CG image | @cg monster_reveal |
@post postId | Deposit a post into the active feed app (social or premium) | @post post_001 |
by: charId | Post author (inside @post block) | by: sarah |
text: "..." | Post caption text (inside @post block) | text: "Morning vibes ☀️" |
free: true | Mark post as always visible — overrides locked_until (premium feeds) | free: true |
locked_until: expr | Condition that must be true to show post — false = blurred lock overlay (premium feeds) | locked_until: player_subscribed_to(vip_feed, creator) |
teaser: "..." | Text shown on the lock overlay when post is locked | teaser: "Subscribe to see more 🔒" |
? comment on app post: | Comment gate — player picks a reply to post in a feed | ? comment on vip_feed post_001: |
? subscribe to appId userId: | Subscription gate — player subscribes to a specific user within a premium feed app | ? subscribe to vip_feed creator: |
? call from charId: | Incoming phone call gate — Answer / Decline branches | ? call from sarah: |
- "hang up" | Optional branch: runs if player ends call early via End Call button | - "hang up" |
@voicemail vmId | Deposit a voicemail into the active phone app | @voicemail vm_missed_01 |
!system cmd | Engine side-effect command (set_time, navigate_to, etc.) — see full list in docs | !system set_time 23:45 |
// comment | Stripped at compile time | // reminder: fix this |
@app is for Simulated OS games only. For Standard VN games the engine defaults to the VN context automatically — you never need to write @app unless you are switching between registered SimOS apps (like @app chatter).
The most common structures, shown with realistic IDs. Swap in the character names, asset filenames, and variable names you've registered in your project.
## The Score@bg classroom // Variables are declared in Studio → Variables before use.// $var — session variable, saved with save slots, reset on new playthrough.// $$var — persistent variable, survives all playthroughs — for global flags, meta-unlocks. // Assign initial values at scene start$score = 0$player_name = "Alex" teacher: "Welcome, class. Let's begin." ? "First question: what is 2 + 2?" - "Four" teacher: "Correct!" // Arithmetic operators: += -= *= /= $score += 10 -> Next Question - "Five" teacher: "...close enough." $score += 3 -> Next Question - "I don't know" teacher: "At least you're honest." -> Next Question ## Next Question? "Capital of France?" - "Paris" $score += 10 -> Results - "London" $score -= 5 -> Results ## Results// Chain conditions with ? if / ? elif / ? else? if $score >= 15: teacher: "Impressive result." // Persistent flag — this player has scored high at least once, ever $$high_scorer = true? elif $score >= 5: teacher: "Not bad."? else: teacher: "We'll keep practicing." @end
// Choices can be conditional — an option only appears when its [$condition] is true.// Conditions read session variables ($var) declared in Studio → Variables. ? "What do you say?" - "I believe you" [$trust >= 1] // This option only shows if $trust is at least 1 player: "I believe you." sarah: "Thank you. Really." // String assignment — use this value in conditionals later $route = "friendship" -> Good Path - "I don't trust you" // No condition — always visible player: "I don't trust you." sarah: "That's fair. But you're wrong." $route = "tense" -> Tense Path - "Stay quiet" [$met_sarah == false] // Only available if player hasn't met sarah yet "You said nothing. Safer that way." -> Quiet Path
// Switch to a messenger-type app registered in Studio → Apps.// All @chat content is deposited into that chat's message log.@app chatter // Open a specific chat — chatId must be registered under this app in Studio.@chat chat_sarah // Messages drip in one at a time as the engine runs sarah: "hey, you there?" sarah: "hello??" // Reply gate — player picks one option to send as their message ? reply: - "sorry, was asleep" player: "sorry just woke up lol" sarah: "okay good, we need to talk" // Track that the player replied — useful for branching later $replied_sarah = true -> Replied - "leave on read" // No player message — sarah gets no response -> Left On Read ## Repliedsarah: "meet me at the usual spot. 20 mins."@end ## Left On Read// Push a notification into the player's notification shade@notify chatter "Sarah" "is waiting for your reply..."@end
// ? if / ? elif / ? else — conditional blocks.// Evaluates against session ($var) or persistent ($$var) variables.// Only the first matching branch executes. ? if $route == "friendship": sarah: "I'm glad we talked." player: "Me too."? elif $route == "tense": sarah: "I hope you change your mind." player: "Maybe."? else: // Fallback — runs when no other branch matched "They stood in silence." // Comparison operators: == != > < >= <=// Logical: and or not// Example multi-condition: ? if $score >= 10 and $met_sarah:
## The Call// Switch to a phone-type app registered in Studio → Apps@app phone // Incoming call gate — player must pick Answer, Decline, or another branch.// Dialogue inside a branch becomes in-call conversation in the Phone app.? call from sarah: - "answer" sarah: "Hey. Are you alone?" player: "Yeah, why?" sarah: "I need you to listen carefully." sarah: "Don't come to the office tomorrow." -> After Call - "decline" // No in-call dialogue — goes straight to the branch body -> Missed Call // 'hang up' is a special branch — runs when player taps End Call mid-conversation - "hang up" sarah: "...hello?" -> Hung Up ## After Call"She hung up before you could ask anything."@end ## Missed Call// Voicemail — deposited into the phone app's voicemail inbox@voicemail vm_sarah_001 from: sarah duration: 0:12 transcript: "It's me. Call me back. Please."@end ## Hung Up"You stared at the phone. Had that been a mistake?"@end
## The Post// Switch to a social_feed-type app registered in Studio → Apps@app social_feed // Posts deposit into the feed as the engine runs — displayed newest-first in the UI.// 'image' is a logical name from Studio → Assets → CGs (no file extension).@post post_morning by: sarah text: "Another late night at the office. Someone send help ☕" image: sarah_office likes: 12 // likeable: true — player can tap the like button likeable: true // Comment gate — player picks a reply to a specific post.// appId and postId must reference what's already been deposited above.? comment on social_feed post_morning: - "Hang in there!" // Player's comment is posted to the thread player: "Hang in there, you've got this!" sarah: "Aw, thanks 🥺" -> After Comment - "Say nothing" // No comment posted — thread stays empty -> After Comment ## After Comment// Push a banner notification into the player's notification shade@notify social_feed "Sarah" "liked your comment""Your notification lit up a moment later."@end
## The Exclusive// Premium feed — requires a premium_feed-type app in Studio → Apps@app vip_feed // free: true — always visible regardless of subscription status@post post_free_001 by: creator text: "Hey everyone, big news coming soon 👀" likes: 204 likeable: true free: true // locked_until — post is blurred with a lock overlay until the condition is true.// player_subscribed_to(appId, userId) is a built-in engine expression.// 'image' is a logical name from Studio → Assets (no file extension).@post post_locked_001 by: creator text: "Here's the full behind-the-scenes video from last night." image: bts_video_thumb locked_until: player_subscribed_to(vip_feed, creator) teaser: "Subscribe to unlock exclusive content 🔒" // ? subscribe to — narrative-driven subscription gate.// Use this when subscription should happen at a specific story beat// rather than passively via the feed header button.? subscribe to vip_feed creator: - "Subscribe" creator: "Thank you so much! 🥺" // Persistent — survives across playthroughs, so future chapters know $$subscribed_vip = true -> Subscribed - "Maybe later" -> Skipped ## Subscribed"The locked post shimmered and revealed itself."@end ## Skipped"The blurred post remained. For now."@end
## The Confrontation// 'rooftop_night' is a logical name from Studio → Assets → Backgrounds@bg rooftop_night // Auto-stage: speaking with () triggers automatic sprite placement.// First speaker goes center; second speaker pushes first to the side.sarah (): "You followed me up here."player (): "You left me no choice." // Named expression — must be registered in the character's sprite map in Studio.// Use () for the default expression, (name) for a registered variant.sarah (angry): "Don't make this harder than it has to be." // Explicit placement — overrides auto-staging, puts marcus in the left slot.// Useful when you need precise control over positioning.@sprite left marcus marcus: "Nobody's going anywhere." // Clear a slot — removes the sprite visually when a character exits.@sprite left clear // sarah and player remain staged in their auto-assigned slots.sarah: "It's just us now." // @end — chapter complete. Use @fin to end the entire story.@end
Standard VN games support character sprites — portrait images that appear over the background while characters speak. Upload sprite images in Assets → Sprites, then link them to characters in the Characters panel under each character's sprite map.
Auto-staging — when a character speaks with sarah (): "text", the engine automatically places them in the best available slot. The first speaker goes center; when a second character speaks, the first moves to the side and the new character takes the other side.
Persistence — sprites stay on screen until you explicitly clear them with @sprite left clear. Scene headers (=== Scene: Name ===) do not clear sprites automatically.
Depth — the character who spoke most recently always floats to the top. Non-speaking characters dim slightly to keep focus on the active speaker.
Expressions — register multiple images per character (e.g. default, happy, angry) in the Characters panel, then reference them with sarah (angry): "text". Falls back to default if the expression isn't found.
| Syntax | What it does |
|---|---|
sarah (): "text" | Auto-stage sarah's default sprite and speak |
sarah (angry): "text" | Auto-stage sarah's angry expression sprite |
@sprite left sarah | Explicitly place sarah in the left slot (overrides auto-staging) |
@sprite center clear | Remove whoever is in the center slot |
sarah: "text" | Dialogue with no sprite — if sarah was already staged, she stays visible but no new staging occurs |
Sprites only appear in the Standard VN template. SimOS games (the phone template) do not render sprites.
When you upload an asset in Creator Studio, it is assigned a logical name — a short identifier without a file extension. Use that logical name directly in your script.
| Asset type | Where to upload | How to use in script |
|---|---|---|
| Background image | Studio → Assets → Backgrounds | @bg your_logical_name |
| CG image | Studio → Assets → CGs | @cg your_logical_name |
| Character sprite | Studio → Assets → Sprites, then link in Characters | sarah (): "text" or @sprite left sarah |
| NPC character | Studio → Characters | sarah: "Hey!" |
| Variable | Studio → Variables | $varName = value or $$varName = value |
| Messenger chat | Studio → Apps → Chats | @chat chat_sarah |
Logical names are set when uploading an asset (defaults to filename without extension). They can be renamed in the Studio asset browser and must be unique per game. For example, a file called city_night.jpg gets the logical name city_night — write @bg city_night in your script, without any extension.
Variables let you track player choices, scores, flags, and any state that shapes the story. Declare them in Studio → Variables first, then read and write them anywhere in your script.
Single dollar sign $var — a save-slot variable. Stored in the player's save and restored whenever they load it. Use $var for almost everything story-related: branching flags, relationship scores, counters, player choices — anything that should feel different on a fresh run but persist across save/load within a playthrough.
Double dollar sign $$var — a persistent variable. Stored in the player's global state, completely separate from save slots. It is never rolled back — even starting a new game leaves it intact. Use $$var for meta-game state that outlives any single playthrough: "has this player ever reached Ending B", global achievement flags, or cross-playthrough unlocks.
| Syntax | Operator | Effect |
|---|---|---|
$score = 0 | = | Set to a literal value or expression |
$score += 10 | += | Add (numbers only) |
$score -= 5 | -= | Subtract (numbers only) |
$score *= 2 | *= | Multiply (numbers only) |
$score /= 2 | /= | Divide (numbers only) |
Reading in conditions — use ? if $var == value: to branch on a variable. Supports ==, !=, >=, <=, >, <.
Interpolation — write "Hello $player_name" inside any dialogue or narration string and the engine substitutes the current value at runtime.
Gated choice options — add [$var >= 1] after an option label to hide it unless the condition is met.
The linter warns if you use a numeric operator (-=, *=, /=) on a variable declared as type string, or if you assign a literal value that doesn’t match the declared type — e.g. assigning "hello" to a number variable. These are warnings, not errors — the script will still compile.
OmiScript is under active development. The engine runs correctly for everything on this page. The features below are either partially implemented or reserved for a future release — don't waste time trying to build them in alpha.
Music and sound effect commands are parsed and compiled but the audio engine is not yet wired — they will be silently ignored at runtime.
Persistent variables can be declared in Studio and assigned with $$varName = value. Assignment is compiled and stored, but reading $$var values in conditions across separate play sessions is not yet wired end-to-end.
Choices inside choice branches may cause unexpected engine behaviour. Keep branching flat for now.
Found something broken that isn't listed here? Report it in Discord or send us an email — we read everything.
Ready to start writing?
Create a free account