This tutorial makes a small playable pack called Router Rescue. It includes a caller problem, a WikiDesk article, a named CRM action and a local trophy. No recording or artwork is required. Finish this small example before adding a campaign or a large voice pack: it teaches identity, references, validation and testing using the same workflow as larger projects.
You need the installed game and a place to store editable local content. Steam is needed only when publishing to Workshop. On Hold's title screen provides Workshop → Create; Mod Studio is also available through the corresponding Options entry. All examples here use content API 1.0.0.
1. Create the local pack
Open Mod Studio and choose the Calls starter. Enter:
| Field | Example | Why it matters |
|---|---|---|
| ID | router-rescue |
Stable identity used by saves, dependencies and namespacing. |
| Name | Router Rescue | Human-readable name displayed in the hub. |
| Author | Your creator name | Attribution shown to players. |
| Description | A helpful router call, guide and trophy. | Explain the content and intended mode. |
The ID must use 3–40 lowercase letters, numbers, underscores or hyphens. It cannot be a reserved object key. Choose it once; the save path and service lock the pack's identity after creation. Use a new ID for a different or cloned pack rather than changing the ID in an established authoring folder.
A starter creates editable local data. It does not subscribe anyone, publish an item or change a saved career. Other starters target languages, office appearances, challenges, campaigns and Night Shift; an empty starter is available when you already understand the format.
2. Understand the three JSON files
mod.json describes the pack. content.json contains the content. achievements.json contains local community trophies. The advanced editor shows a section at a time, so its Scenarios pane expects an array, while the entire content.json file expects an object containing that array.
The following are complete files for this tutorial, not fragments. Replace the example author with your own name. If you enter them through Studio, put each content section in its matching editor; do not paste the full content.json object into the Scenarios array pane.
mod.json
{
"schemaVersion": 2,
"id": "router-rescue",
"name": "Router Rescue",
"author": "Your creator name",
"description": "A helpful router call, knowledge article, CRM action and local trophy.",
"version": "1.0.0",
"gameVersion": ">=1.0.0 <2.0.0",
"contentApi": ">=1.0.0 <2.0.0",
"contentTypes": ["data", "scenarios", "achievements"],
"modes": ["career", "nightshift", "challenge", "campaign", "multiplayer"],
"dependencies": [],
"conflicts": []
}
The game accepts explicit comparison ranges, an exact three-part version or *. Caret (^), tilde (~) and OR (||) ranges are unsupported. This example opts into multiplayer as a simple call expansion; a standalone campaign should declare only the modes its content actually supports.
content.json
{
"scenarios": [
{
"id": "ROUTER_RESTART",
"voice": "ROUTER_RESTART",
"label": "The red router light",
"category": "Internet",
"weight": 3,
"minDay": 1,
"mood": "calm",
"flags": [],
"openers": ["My internet connection has stopped working."],
"clarify": ["The light on my router is red."],
"resolveLines": ["The light is green again. Thank you for helping."],
"details": [],
"solutions": [
{"kb": "ROUTER_HELP", "action": "ROUTER_RESET", "quality": "best"}
],
"lineIds": {
"openers": ["1"],
"clarify": ["01"],
"resolveLines": ["01"],
"details": []
},
"lineIdCounter": 0
}
],
"kbArticles": [
{
"id": "ROUTER_HELP",
"title": "Restart the router connection",
"keywords": ["router", "red light", "connection"],
"steps": [
"Check that the power cable is connected.",
"Restart the router and wait for the connection light to turn green."
],
"agentLine": "Let us restart the router and wait for the connection light to turn green."
}
],
"crmActions": {
"ROUTER_RESET": {
"label": "Reset router connection",
"desc": "Apply the router connection reset after checking the account."
}
}
}
achievements.json
[
{
"id": "router_helper",
"name": "Back online",
"desc": "Resolve the Router Rescue call with a happy customer.",
"hidden": false,
"trigger": {
"type": "resolveScenario",
"scenario": "ROUTER_RESTART",
"minCsat": 4
}
}
]
The solution links two local IDs: the knowledge article and CRM action. The loader turns owned content into namespaced runtime IDs such as router-rescue:ROUTER_HELP. Within this pack, author the short local ID; you do not need to manually rewrite all references with your pack prefix.
A custom CRM action declares a named action recognized by this pack's solution. It does not let you attach a function or an arbitrary account mutation. Larger authored outcomes belong in the bounded rule system described in Stories and campaigns.
3. Build the same call in guided forms
The call designer handles normal opener, clarification and resolution pools. Set the call ID and voice key, label, supported category, calm/annoyed/angry mood, relative weight and minimum day. Add at least one opener and resolution line for an ordinary non-graph call. Keep responseMenu absent for this KB/CRM example: a response-menu call uses canned reply choices instead of the ordinary solution workflow.
Open Content editors to add the WikiDesk article and CRM action. The article needs a title and a nonempty list of steps. Keywords improve search; agentLine is the explanation spoken when the article is used. Return to the call designer and select the local article/action as its solution. The reference list includes stock content and available local IDs, avoiding a guessed reference.
For a knowledge-only call, its solution can use "action": null; for an action-only call it can use no knowledge reference. A solution quality is best, good or ok. This tutorial uses a combined solution to demonstrate both systems. Treat solution quality as authored expected outcome, then test the player's actual route through the interface.
Use the advanced JSON pane for fields not shown in a guided form. Guided editing retains other fields; you can combine ordinary form edits with advanced data instead of maintaining a separate file format. A JSON parsing error means the section has not been captured as valid data. Correct brackets, commas, quoted strings and the expected array/object shape before saving.
4. Validate and save
Choose Validate pack. Errors identify a file/field path such as content.json.scenarios[0].solutions[0].kb. Read the path before changing data: an unknown article reference is different from a missing recording.
Validation checks the full pack contract: identity, compatibility, reference structure, supported categories/values, stable line IDs, bounds, graphs, rules and presentation descriptors. Saving uses a validated pack transaction. Export and publication additionally check the actual referenced assets. A path in JSON is not enough if the image/recording has not been imported.
Warnings can describe a permitted but significant action, such as overriding a stock career day. Errors block a valid pack. Fix all errors, save, and validate again after any rename or reference edit. Changing ROUTER_HELP to ROUTER_GUIDE requires updating the solution too.
5. Run a scratch test
Choose Test this call. Set a reproducible seed, an eligible day, caller variant and independent subtitle/audio languages. Seed accepts an unsigned 32-bit number from 0 to 4,294,967,295; day accepts 1–10,000. u means the seeded default caller selection, while m/f selects a gender variant.
In the test, answer and work through the normal call: identify the issue, verify the account, find the local WikiDesk article and optionally explain it. Choose Propose Fix or Proceed with Fix, then perform Reset router connection on the verified account in CustomerDeck. Confirm the resolution and wrap up. Try a wrong or incomplete route too; a useful call gives the player enough information to discover its intended solution.
The debug panel displays the scenario, selection pool, diagnosis/verification, solutions, graph/rule state and selected recording or subtitle fallback. Rerun call can exercise another diagnostic cause; Rerun shift resets owned state and scheduling for the chosen seed. Test Shift also checks integration into a real office session.
Scratch tests do not write career or Cloud saves, grant Steam awards, unlock community trophies or retain custom result records. The tutorial trophy will therefore not unlock in this test. Temporary subtitle/audio choices are restored when leaving the test. To verify a trophy, later use an ordinary eligible session with the pack enabled rather than assuming its absence in scratch means failure.
6. Keep recording identity stable
Recordings are optional. Unrecorded lines remain playable with timed subtitles. The lineIds tokens in the tutorial belong to the text entries, not their current array positions. When moving the opener or clarification, move its token with it. The designer does this automatically.
An English female caller opener might be named fm_C-ROUTER_RESTART-OPEN-1.mp3; the article's female agent explanation uses f_A-KB-ROUTER_HELP.mp3. Export the recording CSV and use Studio's exact target filenames instead of building names from memory. Use Recording coverage, select a language/actor, and import a named batch when ready. Languages, audio and office packs covers the naming rules and fallback order.
Do not recycle an ID from a deleted published line for a different sentence. A subscriber with an older recording must not hear the wrong words because an array position was reused. The editor's monotonic lineIdCounter helps keep new tokens distinct.
7. Recover, duplicate and share
The designer and content editor provide undo/redo. Unsaved work is also stored in the local studio-draft.json recovery file. Reopening a pack can recover or discard it. A recovery draft is an aid to editing, not a published runtime file or a substitute for backups. Export a validated ZIP before a major restructure.
Duplicate a call to explore a second problem. Studio assigns a distinct call/voice ID; rename its visible label and update its solutions. Duplicating does not automatically record new dialogue. Keep unused experimental calls in a separate development copy if you do not want them in the release pool.
To share offline, export ZIP and import that ZIP into a clean profile to verify the package you will actually send. To publish on Workshop, import a preview, supply a clear description, choose visibility and review the publication change preview. New items default to Private. A detailed publishing checklist is in Sharing, testing and troubleshooting.
The reusable Beginner Calls starter ZIP supplies another complete first-call example and its reuse terms. Import it through the in-game ZIP action; it uses its own pack ID and a knowledge-only solution, while Router Rescue above demonstrates a combined knowledge/CRM solution. For your own pack include a README explaining modes, dependencies and installation, and a license/permission statement for anything others may reuse. Once this tutorial works, use the content reference to add further capabilities without inventing unsupported fields.
