Skip to content

Ink Scripting Guide

Ink is a powerful scripting language for writing interactive narratives. This guide covers how to use Ink for Encounters stories.

Ink Basics

What is Ink?

Ink is a narrative scripting language created by Inkle Studios. It allows you to write branching stories with:

  • Linear narrative sequences
  • Player choices
  • Variables and state tracking
  • Conditional logic
  • Functions and reusable content

For comprehensive Ink documentation, see the official Ink guide.

Encounters-Specific Features

Encounters extends Ink with special tags for messaging features. Tags are added to the end of lines using the # symbol. Multiple tags can be combined on a single message.

Complete Tag Reference

Display and State Tags

#initial - Marks messages that appear when the conversation first loads (already sent before player joins):

ink
Hey Sarah, have you seen my keys? #initial #read
Sarah? You were supposed to meet me an hour ago. #initial

Use this for messages that happened before the player started interacting with the story.

#read - Marks messages as already read by the player:

ink
Like my new avatar? #initial #read

Combine with #initial to show messages that the player has already seen.

#me - Indicates the message is from the player (not the character):

ink
Looks great! And lol that video 😂 #me #initial #read

Use this to show the player's previous responses in the conversation history.

Timing Tags

#typing:Xs - Shows typing indicator for X time before message appears:

ink
Let me check... #typing:3s
I found something! #typing:2s
This is taking a while... #typing:10s
I'm writing a long message... #typing:2m

Time units:

  • s - seconds (e.g., #typing:5s)
  • m - minutes (e.g., #typing:2m)
  • h - hours (e.g., #typing:1h)

Makes conversations feel more realistic by simulating the character typing.

#delay:Xs - Delays message appearance by X time:

ink
Mum just called the police. #delay:4s
I need to think about this. #delay:5s
Let me drive over there. #delay:15m
I just got back from the police station. #delay:2h

Time units:

  • s - seconds (e.g., #delay:10s)
  • m - minutes (e.g., #delay:5m)
  • h - hours (e.g., #delay:1h)

Use this to create pauses in the conversation, building tension or allowing time to pass.

Combining Timing Tags

Use both tags together: #typing:3s #delay:4s means:

  1. Wait 4 seconds (delay)
  2. Show typing indicator for 3 seconds
  3. Display the message

This creates the most realistic messaging experience!

Media Tags

#image:filename.jpg - Displays an image in the message:

ink
Like my new avatar? #initial #read #image:alex-avatar.jpg
Check out what I found! #image:evidence.jpg

The image file must exist in your src/assets/ directory.

#link:url - Creates a clickable link in the message:

ink
Have you seen this funny youtube video? #link:www.youtube.com/watch?v=dQw4w9WgXcQ
Check out this article: #link:https://example.com/article

Players can tap/click the message to open the link.

Group Chat Tags

#contact:contact-id - In group conversations, specifies who sent the message:

ink
Has anyone heard from Sarah? #contact:alex
I've been trying to call her all morning. #contact:mum

The contact-id must match a contact ID from your src/contacts/ directory.

Group Chats Only

The #contact: tag only works in group conversations (where type: "group" in the conversation JSON). Don't use it in individual conversations.

Cross-Conversation Tags

#notify:conversation-id:knot-name - Triggers a knot in another conversation:

ink
// In mum.ink
I'm calling the police now! #notify:alex:on_mum_police_contacted

// In alex.ink
== on_mum_police_contacted ==
Mum just called the police. #typing:3s #delay:4s
They're opening a missing person case.
-> END

This allows events in one conversation to trigger responses in another, creating interconnected storylines.

#unlock:conversation-id - Unlocks a new conversation for the player:

ink
Let me add Alex to our conversation. #delay:3s #unlock:family-group

The conversation ID must match a conversation file in src/conversations/. The unlocked conversation will appear in the player's conversation list.

Creating Dynamic Stories

Use #notify: and #unlock: to create stories that evolve across multiple conversations:

  • Unlock new contacts as the story progresses
  • Have characters react to events in other conversations
  • Create group chats that form during the story

Tag Combination Examples

Here are some common tag combinations:

Initial conversation history:

ink
Hey! How are you? #initial #read
I'm good, thanks! #me #initial #read
Great to hear! #initial #read

Realistic typing with delay:

ink
Let me check something... #typing:3s #delay:2s
Oh no. #typing:1s #delay:3s
You need to see this. #image:evidence.jpg #typing:2s

Group chat with multiple speakers:

ink
Has anyone seen Sarah? #contact:alex #delay:2s
No, I haven't heard from her. #contact:mum #delay:5s
This is really worrying. #contact:alex #delay:3s

Triggering events across conversations:

ink
I'm going to call the police right now. #typing:2s
They said they'll open a case. #notify:alex:on_police_called #delay:5s

Unlocking new conversations:

ink
I think we should create a group chat with everyone. #typing:3s
Adding Alex and Dad now... #delay:2s #unlock:family-group

Story Structure

Basic Template

Every Ink file should follow this structure:

ink
// Variables at the top
VAR variable_name = false
VAR conversation_state = "initial"

// Entry point
-> start

== start ==
// Initial messages
First message #initial #read
Second message #initial
-> player_choices

== player_choices ==
// Player choice options
* [First choice]
  -> first_response
* [Second choice]
  -> second_response
-> END

== first_response ==
Character's response to first choice
-> END

== second_response ==
Character's response to second choice
-> END

Variables

Declare variables at the top of your file:

ink
VAR police_contacted = false
VAR alex_knows_player = false
VAR sarah_location_known = false
VAR conversation_state = "initial"

Variable Types:

  • Boolean: true or false
  • Number: 0, 1, 42
  • String: "initial", "waiting"

Using Variables:

ink
~ police_contacted = true
~ conversation_state = "police_discussion"

Knots (Sections)

Knots are sections of your story, defined with ==:

ink
== start ==
This is the start knot
-> next_knot

== next_knot ==
This is another knot
-> END

Naming Convention:

  • Use lowercase with underscores: start, player_choices, found_phone_response
  • Be descriptive: alex_police_response is better than response1

Choices

Player choices are defined with *:

ink
== conversation_choices ==
* [Hi, I found Sarah's phone]
  -> found_phone_response
* [Where was Sarah last seen?]
  -> last_seen_response
* [We should call the police]
  -> police_response
-> END

Choice Text:

  • Text in brackets [] is what the player sees
  • Keep choices concise and clear
  • Offer meaningful alternatives

Conditional Choices

Show choices only when conditions are met:

ink
* {alex_knows_player} [Tell me more about Sarah's friends]
  -> friends_response
* {not police_contacted} [Should we call the police?]
  -> police_discussion

Operators:

  • {variable} - True if variable is true or non-zero
  • {not variable} - True if variable is false or zero
  • {variable == value} - Equality check
  • {variable > value} - Greater than
  • {variable and other_variable} - Both true

Advanced Patterns

Conversation State Management

Track where the player is in the conversation:

ink
VAR conversation_state = "initial"

== start ==
Initial messages here
~ conversation_state = "waiting_for_response"
-> conversation_choices

== conversation_choices ==
* [Choice 1]
  ~ conversation_state = "responded_to_choice_1"
  -> response_1

Cross-Conversation Variables

Variables can be shared between conversations for complex stories:

ink
// In mum.ink
== on_police_called ==
I'm calling the police now!
~ police_contacted = true
-> END

// In alex.ink
== check_police_status ==
{police_contacted:
  I heard mum called the police.
- else:
  Should we call the police?
}
-> END

Triggered Events

Create knots that other conversations can trigger:

ink
== on_mum_police_contacted ==
Mum just called the police. #typing:3s #delay:4s
They said they're opening a missing person case now.
~ conversation_state = "reacted_to_police"
-> END

This knot can be triggered by events in other conversations.

Sequences

Show different content on repeated visits:

ink
== repeated_question ==
{
- First time asking
- Second time asking
- Third time asking
- They keep asking...
}
-> END

Conditional Text

Inline conditions within messages:

ink
{police_contacted: The police are already involved. | Maybe we should call the police.}

Example: Complete Conversation

Here's a complete example showing best practices:

ink
// alex.ink - Conversation with Alex about missing Sarah

// State variables
VAR police_contacted = false
VAR alex_knows_player = false
VAR club_info_shared = false
VAR conversation_state = "initial"

-> start

== start ==
// Initial messages that appear when conversation opens
Like my new avatar? #initial #read #image:alex-avatar.jpg
Have you seen this funny video? #link:www.youtube.com/watch?v=dQw4w9WgXcQ #initial #read
Looks great! And lol that video 😂 #me #initial #read
Hey Sarah, have you seen my keys? #initial #read
I think they're on the kitchen counter #me #initial #read
Sarah? You were supposed to meet me an hour ago. Where are you? #initial
This is really weird... you always text back immediately. #initial

// Transition to interactive mode
~ conversation_state = "waiting_for_response"
-> conversation_choices

== conversation_choices ==
// Player's available choices
* [Hi, I found Sarah's phone and saw your messages]
  -> found_phone_response
* [Where was Sarah last seen?]
  -> last_seen_response
* {alex_knows_player and not club_info_shared} [Tell me more about the club]
  -> club_details_response
* {not police_contacted} [We should call the police]
  -> police_response
-> END

== found_phone_response ==
Wait... who is this? #typing:2s
You're texting from Sarah's phone but you're not Sarah. #typing:1s
Did you find her phone? Oh god, something's happened to her hasn't it? #typing:3s
~ alex_knows_player = true
~ conversation_state = "phone_explained"
-> END

== last_seen_response ==
She was at Club Neon last night. #typing:2s
Said she was meeting some new friends from work. #typing:2s
I offered to pick her up but she said she had a ride. #typing:1s
That was around 11 PM.
~ conversation_state = "shared_info"
-> END

== club_details_response ==
Club Neon is on King Street in the city centre. #typing:2s
It's pretty new, opened about a month ago. #typing:2s
Sarah said her colleagues Emma and Jake were going to meet her there.
~ club_info_shared = true
~ conversation_state = "shared_club_info"
-> END

== police_response ==
Yes, you're right. I should have called them already. #typing:2s
But they always say to wait 24 hours... #typing:1s
It's been about 18 hours now.
~ conversation_state = "police_discussion"
-> END

// Event triggered from another conversation
== on_mum_police_contacted ==
Mum just called the police. #typing:3s #delay:4s
They said they're opening a missing person case now.
~ police_contacted = true
~ conversation_state = "reacted_to_police"
-> END

Best Practices

Writing Style

Natural Dialogue

Write messages as real people would text - use contractions, casual language, and emojis where appropriate.

Good:

ink
Hey! Where are you? 😊
I'm getting worried...

Avoid:

ink
Hello. I am inquiring about your current location.
I am experiencing feelings of concern.

Pacing

  • Use timing tags to create realistic conversation flow
  • Don't overwhelm the player with too many messages at once
  • Build tension with delays and typing indicators
  • Vary timing - quick responses for urgent moments, longer delays for thinking
ink
Let me check something... #typing:3s #delay:2s
Oh no. #typing:1s #delay:3s
You need to see this. #image:evidence.jpg #typing:2s

Timing Guidelines:

  • Short messages: 1-2 seconds typing
  • Medium messages: 2-4 seconds typing
  • Long messages: 4-6 seconds typing
  • Very long messages: 1-2 minutes typing
  • Quick pause: 1-3 seconds delay
  • Emotional pauses: 3-10 seconds delay
  • Short activity: 1-5 minutes delay (driving, searching, etc.)
  • Longer activity: 10-60 minutes delay (meetings, investigations)
  • Major time jumps: 1+ hours delay (next day, later that evening)

Choice Design

  • Offer 2-4 choices at a time (avoid overwhelming)
  • Make choices meaningful - they should affect the story
  • Use clear language - player should understand what each choice means
  • Vary choice types - questions, actions, emotional responses

State Management

  • Track important decisions with variables
  • Use descriptive state names: "police_contacted" not "state1"
  • Update state consistently when story progresses

Testing

  • Test all branches - make sure every choice works
  • Check timing - ensure delays feel natural
  • Verify conditions - conditional choices should appear correctly
  • Test cross-conversation events if you use them

Common Patterns

Delayed Revelation

ink
I need to tell you something... #typing:3s #delay:2s
It's about Sarah. #typing:2s #delay:3s
She's been lying to us. #typing:2s

Information Gathering

ink
* [What time did she leave?]
  ~ time_asked = true
  Around 11 PM, I think.
  -> more_questions
* [Who was she with?]
  ~ companions_asked = true
  Some people from work.
  -> more_questions
  
== more_questions ==
* {time_asked and companions_asked} [That's all I need to know]
  -> END
* {not time_asked} [What time did she leave?]
  -> time_response

Emotional Progression

ink
VAR trust_level = 0

* [I want to help find Sarah]
  ~ trust_level = trust_level + 1
  Thank you... I really appreciate that.
  
* [Tell me everything you know]
  {trust_level > 2:
    Okay, I trust you. Here's what happened...
  - else:
    I don't know if I should tell you everything yet.
  }

Debugging Tips

Common Errors

Missing -> END:

ink
== my_knot ==
Some text here
// ERROR: No ending!

Fix:

ink
== my_knot ==
Some text here
-> END

Undefined knot:

ink
-> non_existent_knot  // ERROR: Knot doesn't exist

Syntax errors in conditions:

ink
{variable = true}  // ERROR: Use == for comparison
{variable == true}  // CORRECT

Testing Your Ink

Build your story to check for errors:

bash
npm run build:story

The build script will show compilation errors with line numbers.

Quick Reference: All Encounters Tags

Here's a complete list of all available tags for quick reference:

TagDescriptionExample
#initialMessage exists before player joinsHey! #initial
#readMark message as already readHi there #initial #read
#meMessage from the playerThanks! #me #initial #read
#typing:XShow typing indicator (s/m/h)Let me check... #typing:3s or #typing:2m
#delay:XWait before showing message (s/m/h)I found it! #delay:5s or #delay:10m
#image:filenameDisplay an imageLook at this! #image:photo.jpg
#link:urlCreate a clickable linkCheck this out #link:example.com
#contact:idSpecify sender in group chatHello everyone #contact:alex
#notify:conv:knotTrigger knot in another conversationDone! #notify:alex:on_event
#unlock:conv-idUnlock a new conversationAdding them now #unlock:group-chat

Tag Compatibility

Can be combined:

  • #initial + #read + #me - Show player's past messages
  • #typing:Xs + #delay:Xs - Realistic message timing
  • #image:file + #typing:Xs - Image with typing indicator
  • #delay:Xs + #unlock:id - Unlock conversation after delay
  • #notify:conv:knot + #typing:Xs - Trigger with timing

Group chat specific:

  • #contact:id - Only use in group conversations

Cross-conversation:

  • #notify:conv:knot - Trigger events in other conversations
  • #unlock:conv-id - Make new conversations available

Visual Studio Code with Ink Extension

The best way to write Ink scripts is using Visual Studio Code with the Ink extension:

Why VS Code + Ink Extension?

  • Syntax highlighting for .ink files
  • Auto-completion for Ink keywords
  • Real-time error detection
  • Jump to knot definitions
  • Outline view of your story structure

Installation:

  1. Download Visual Studio Code (free)
  2. Install VS Code
  3. Open VS Code
  4. Click Extensions icon (left sidebar) or press Ctrl+Shift+X / Cmd+Shift+X
  5. Search for "Ink" by inkle
  6. Click Install

Using the Extension:

  • Open any .ink file and it will automatically highlight syntax
  • Hover over knots to see previews
  • Use Ctrl+Click / Cmd+Click on knot names to jump to them
  • View story structure in the Outline panel

Other Resources

Next Steps

Released under the MIT License.