Skip to content

The settings file

Everything you build in CroStream's screens (macros, triggers, queues, pinned Dashboard buttons, integration settings and your preferences) is saved in one file, config.json, in the config folder (see Where files are kept). You never need to open it; the app writes it for you. This page is for when you want to: edit many macros at once, fix something by hand, copy a setup between machines, generate macros from a script, or understand what the app saved.

Overlays, widgets, your media, viewers and history are not in this file; see Other files.

Editing it safely

  1. Close CroStream first, or be ready to reload. CroStream keeps the settings in memory and writes the whole file whenever you change something in the app, so an unsaved hand edit is overwritten by the next change you make in the UI.
  2. Edit the file in any text editor. It's ordinary JSON, written with two-space indents.
  3. Load it. Start CroStream, or, if it's running, click Reload on the Triggers or Macros page (or call ReloadConfig). Reload applies macros, triggers, queues, integration settings, history and viewer settings at once, without a restart.

Before every change it makes, CroStream copies the current file into the backups folder next to it (named like config-20261011T151943.647Z.json). It keeps the newest 20 copies, plus the newest copy of each of the last 14 days. Restore one from Settings → Backups; see Backups.

Settings → Backups, listing the automatic copies of the settings file with a Restore button each

A broken file stops saving, not streaming

If config.json isn't valid when CroStream starts, CroStream starts anyway with the default setup in memory, shows the error, and refuses to save anything, so it can't overwrite your file. Fix the file and click Reload. If a reload finds the file invalid, the error is shown and the running setup stays as it was.

Two kinds of problem are treated differently:

  • Structural problems refuse the whole file: invalid JSON, an unknown key anywhere outside params and integrations, a wrong version, a malformed or duplicate ID, two macros (or triggers, or queues) with the same name, a trigger that names a macro that doesn't exist, a step breaking the step rules below.
  • Problems with one item only disable that item: an unknown action or trigger type, an unknown provider, or params the action or trigger doesn't accept (including unknown parameter names). The rest of your setup loads, and the app shows the item's error on the Triggers or Macros page.

The whole file

{
  "version": 3,
  "integrations": { "twitch": {}, "obs": { "host": "localhost", "port": 4455, "auto_connect": false } },
  "sequences": [ … ],
  "bindings": [ … ],
  "queues": [ … ],
  "history": { "keep_days": 90 },
  "viewers": { "currency": "pixels" },
  "dashboard": { "macros": [ "seq-…" ] },
  "webhooks": { "exposure": "lan", "inbound": [ … ], "destinations": [ … ] },
  "desktop": { "close_to_tray": true },
  "updates": { "channel": "stable" },
  "diagnostics": { "reports": true }
}
Key Holds Section
version Always 3
integrations Each integration's settings Integrations
sequences Your macros Macros
bindings Your triggers Triggers
queues Your queues (left out when there are none) Queues
history History settings History
viewers Viewer database settings Viewers
dashboard Pinned Dashboard macros Dashboard
webhooks The webhook listener, incoming webhooks and destinations (left out when there are none) Webhooks
desktop Tray, start-up and notification preferences Desktop
updates How the app updates Updates
diagnostics Crash reporting opt-ins (left out until you turn something on) Diagnostics

Sections at their defaults are left out of the file. A file from an older version (1 or 2, or from before the app was called CroStream) is upgraded when CroStream starts, after it saves a copy as config.v1.json.bak or config.v2.json.bak. See Upgrading.

What the file calls things

The file uses CroStream's internal names:

In the app In the file
Macro sequence (sequences)
Trigger binding (bindings)
A trigger's event its source
Action action step

Durations are text in Go's format: "500ms", "30s", "5m", "1h30m". The app writes them in full ("1m0s"). IDs are a lowercase prefix and a 26-character ULID: seq-01m4nr3cqyykwt94nac1zkhpm6. New items need a fresh, unique ID; the easiest way to get one is to create the item in the app. IDs never change, so references between items (a trigger's macro, a step's queue) survive renames.

Integrations

integrations holds each integration's settings by its ID. Secrets (the OBS password and login tokens) are never in this file; they're kept in the secrets folder of the config folder.

ID Keys
obs host (default "localhost"), port (default 4455), auto_connect (Connect automatically)
twitch none (the login is a secret)
steam path: the Steam folder, when it isn't found by itself
discord client_id: your own Discord application ID, if you use one

Settings of integrations this version doesn't know are kept as they are.

Macros

Each entry of sequences is a macro:

{
  "id": "seq-01m4nr3cmax6btz4tr8jgjfffx",
  "name": "Sub thanks",
  "timeout": "1m0s",
  "continue_on_error": true,
  "steps": [
    { "id": "alert", "kind": "action", "action": "overlay.alert", "providers": ["overlay"],
      "params": { "target": "overlay-01m4nr1t2xz2zw461bq7erpa90/element-gp8bzatyemrn4653e31n7khfx9", "hold": "8s" } },
    { "id": "wait", "kind": "delay", "delay": "2s" },
    { "id": "thanks", "kind": "action", "action": "chat.send", "providers": ["twitch"],
      "params": { "message": "Thanks for the sub, {user}!" } }
  ]
}
Key Meaning
id seq-<ulid>
name Unique among macros
timeout How long one run may take: default 30s, at most 10m (longer values are cut to 10 minutes)
continue_on_error Keep running later steps after a step fails. The run still counts as failed, so a redemption is refunded.
steps The steps, in order

Steps

Every step has an id (a lowercase letter, then lowercase letters, digits and _) that's unique within the macro, branches included: it names the step's outputs, as in {thanks.message_id}. Its kind is one of:

kind Keys Does
action action, providers, params Runs an action
delay delay (more than 0, at most 10m) Waits
if branches, else Runs the first branch whose condition holds, else the else steps
switch value, cases, else Runs the first case whose values include value
stop fail, message Ends the run here; with "fail": true it counts as failed
{
  "id": "tier", "kind": "switch", "value": "{tier}",
  "cases": [
    { "values": ["3"], "steps": [ { "id": "t3", "kind": "action", "action": "chat.send", "params": { "message": "TIER 3?!" } } ] },
    { "values": ["2", "prime"], "steps": [ { "id": "t2", "kind": "action", "action": "chat.send", "params": { "message": "Thank you!" } } ] }
  ],
  "else": [ { "id": "done", "kind": "stop", "message": "tier 1" } ]
}

switch compares value (after filling in variables) with each case's values as text, ignoring case and surrounding spaces. Only if and switch steps may hold other steps. A macro has at most 500 steps in all, nested at most 8 levels deep.

Conditions

An if step's branches are { "when": <condition>, "steps": [...] }. A condition is:

{
  "match": "all",
  "rules": [
    { "left": "{bits}", "op": "gte", "right": "100" },
    { "fact": "obs.scene", "op": "eq", "right": "Gameplay" },
    { "not": true, "left": "{user_role}", "op": "role_gte", "right": "moderator" },
    { "group": { "match": "any", "rules": [ … ] } }
  ]
}

match is all or any. A rule compares left (a template such as {bits}) or a fact (a live value: obs.scene, or <name>:<argument> such as tools.counter:deaths) with right; not inverts it, and group nests a condition. An empty condition holds.

op True when
eq, ne Equal, not equal (as numbers when both sides are numbers, else as text ignoring case)
contains, starts, ends The left side contains, starts with, ends with the right
gt, gte, lt, lte A number above, at least, below, at most the right
empty The left side is empty (right unused)
in The left side equals one of the right side's values (comma- or line-separated)
matches The left side matches the regular expression on the right, ignoring case
role_gte The left side, a role, is at least the right one

The facts you can check are listed in Conditions.

Actions, steps and providers

An action step names the action by its ID. Shared actions have no prefix and run on every integration that provides them: clip (OBS replay and Twitch clip), marker, chat.send, scene.set. Provider actions belong to one integration: twitch.timeout, obs.audio_mute, tools.counter, overlay.alert, viewers.points.

providers limits a step to the listed integrations (["obs"]). Empty or left out, the step runs on every connected integration that provides the action. The full list of actions, with their IDs, parameters and outputs, is in Actions; the HTTP API's GetCatalog lists them too.

Action params

params holds the action's parameters by their file names, which are shown in Actions. Most parameters are text and take {variables}, even ones that hold a number ("amount": "500" or "amount": "{bits}"); a few are plain numbers or true/false. When unsure, build the step in the app and copy what it saved. A parameter the action doesn't know, or a value of the wrong type, disables the macro.

{ "id": "points", "kind": "action", "action": "viewers.points", "providers": ["viewers"],
  "params": { "viewer": "{user_login}", "change": "add", "amount": "500", "reason": "Subscribed" } }

Variable naming and JSON params

Every step's outputs become variables for the steps after it, under three names:

  • {step.provider.key}, always: {twitch_clip.twitch.clip_url};
  • {step.key}, from the first provider that succeeded, in manifest order (Twitch, OBS Studio, Steam, Discord, Queues, Tools, Viewers, Overlays), so it's set whenever any provider succeeded: {twitch_clip.clip_url};
  • {key} alone, likewise, unless the trigger already set a variable of that name: the trigger's variables are never overwritten.

Parameters that hold JSON (OBS filter settings, OBS plugin requests) fill variables into string values only, so text from viewers can't add keys. See Variables.

Triggers

Each entry of bindings is a trigger:

{
  "id": "bind-01m4nqwwap6z14j4pdpt7tkjkm",
  "name": "clip",
  "source": {
    "type": "twitch.chat_command",
    "params": { "command": "!clip", "aliases": ["!c"] },
    "min_role": "everyone",
    "cooldown": "1m0s",
    "user_cooldown": "30s"
  },
  "sequence": "seq-01m4nqwwap513e991pfg4hfsdx"
}
Key Meaning
id bind-<ulid>; cooldowns are kept by it
name Unique among triggers
disabled true turns the trigger off
source.type The event, below
source.params The event's options
source.min_role Who may fire it: everyone, subscriber, vip, moderator or broadcaster
source.cooldown Time before it fires again for anyone
source.user_cooldown Time before the same viewer can fire it again
sequence The ID of the macro it runs

The broadcaster isn't held back by cooldowns.

source.type params (* required)
twitch.chat_command command* (no spaces), aliases
twitch.redemption reward_id (empty: any reward), input_contains
twitch.follow none
twitch.sub tier (any, 1, 2, 3, prime), min_months, gifted (exclude default, include, only)
twitch.gift tier (any, 1, 2, 3), min_count, max_count
twitch.cheer kind (any, cheer, power_up), min_bits, max_bits
twitch.raid min_viewers, max_viewers
twitch.stream state* (online, offline)
twitch.channel_update none
twitch.ad_break none
twitch.hype_train phase* (begin, level_up, progress, end)
twitch.poll_end none
twitch.prediction_end status (any, resolved, canceled)
twitch.auto_reward reward (any, send_highlighted_message, single_message_bypass_sub_mode, random_sub_emote_unlock, chosen_sub_emote_unlock, chosen_modified_sub_emote_unlock)
steam.game_started, steam.game_stopped, steam.screenshot game
hotkey.press keys* (such as Ctrl+Shift+F9)
webhooks.received hook* (an incoming webhook's ID)
queue.release queue* (a queue ID)
tools.interval every* (1m to 24h, default 10m), only_live (default true), min_chat (0 to 1000; 0 is off)
viewers.first_chat none
viewers.returning min_days_away (0 to 365)

Older files may have reward_title on redemptions; it still works, but new triggers pick the reward by reward_id. What each event's options do and the variables it sets are in Events.

Queues

Each entry of queues is a queue:

{
  "id": "queue-01m4nr38z22pjm7w7rdynxvqsd",
  "name": "Song requests",
  "interval": "3m0s",
  "per_release": 1,
  "max_items": 25,
  "when_full": "refuse",
  "fields": [ { "key": "song", "label": "Song", "default": "{args}" } ],
  "keep_waiting": true,
  "keep_for": "2h0m0s"
}
Key Meaning
id, name queue-<ulid>; a unique name
interval Time between releases: 1s to 24h
per_release Items released each time: 0 to 100 (0 means 1)
max_items Most waiting items: 0 to 1000 (0 means 50)
when_full refuse (default: the Add to queue step fails) or drop_oldest
fields Up to 20 values each item carries: key (named like a step ID), label, and default (a template used when the Add to queue step leaves it empty)
keep_waiting Keep waiting items through a restart (After a restart); otherwise they're cleared
keep_for With keep_waiting, drop restored items older than this: 1m to 7 days (168h), or empty for no limit

Webhooks

Webhooks: who may reach the listener, the incoming webhooks and the outgoing destinations. Tokens, signing keys and destination secrets are not here; they are in the secrets store.

"webhooks": {
  "exposure": "lan",
  "port": 62690,
  "inbound": [
    {
      "id": "hook-01m4p7bnmvwx8wn3q65kdztznk",
      "name": "Ko-fi",
      "slug": "ko-fi",
      "enabled": true,
      "format": "form",
      "rules": [
        { "var": "donor", "path": "data.from_name" },
        { "var": "amount", "path": "data.amount", "type": "number" },
        { "var": "message", "path": "data.message", "default": "" }
      ],
      "reply": { "status": 202 }
    }
  ],
  "destinations": [
    {
      "id": "dest-01m4p7bnpdsagyspgnm362t8r2",
      "name": "Discord relay",
      "url": "https://relay.example.com/crostream",
      "auth": "bearer",
      "headers": ["X-Source: CroStream"]
    }
  ]
}
Key Meaning
exposure local (the default, left out), lan (local networks) or open (anyone)
port The listener's port, 1024 to 65535; left out for 62690
inbound The incoming webhooks, at most 100
destinations The outgoing destinations, at most 100

An entry of inbound:

Key Meaning
id, name hook-<ulid>; a unique name of at most 80 characters
slug The address, /hook/<slug>: 1 to 64 of a-z, 0-9 and -, starting with a letter or digit; unique
enabled false makes it answer 404
methods Accepted methods: GET, POST, PUT, PATCH or DELETE; left out for POST only
format auto (the default), text, json or form
auth token (the default), hmac, token+hmac or none. none is refused unless exposure is local
hmac_header The signature header; left out for X-Signature-256
rules The mapping rules, at most 100: var (^[a-z][a-z0-9_]*$, and not body, content_type, method, hook, remote or detail), path (a path), type (string, number, bool or json), many (join, count, first, last or json), sep and default
sample The sample body the editor maps from, up to 64 KiB
reply status (200 to 299, left out for 202), type (json or text) and body (a template, up to 64 KiB)

An entry of destinations:

Key Meaning
id, name dest-<ulid>; a unique name
url The base address, http:// or https://, with no user name or password
auth none (the default), bearer, basic, hmac or header
auth_header The header for header (required there) and hmac (default X-Signature-256)
headers Up to 32 fixed headers, each "Name: Value"

A webhook you add by hand has no token yet, and refuses every call until it gets one: open it in the app and save it, which makes the missing secrets.

The Send a webhook step ("action": "webhooks.send", "providers": ["webhooks"]) takes these params: destination (a destination ID), url, method, headers (a list of "Name: Value" lines), body_type (none, text, json, form or json_raw), text, json (a list of {"path", "type", "value"} fields; type is string, number, bool, null or json), json_raw, form (an object of names and values), response (a list of rules like the above), timeout, retries and fail_on_error.

History

"history": { "off": false, "keep_days": 90 }

off: true stops recording and reading history, leaving the database as it is. keep_days is 1 to 3650; 0 or left out means 90.

Viewers

"viewers": { "off": false, "currency": "pixels", "chat_keep_days": 0, "ignore": ["nightbot", "streamelements"] }
Key Meaning
off true stops recording viewers and turns the points actions off; the database is left as it is
currency What points are called, up to 32 characters (empty: points)
chat_keep_days How long viewers' chat messages are kept: 0 (forever) to 3650. Message counts are kept either way
ignore Up to 500 lowercase Twitch logins (bots, your alt accounts) left out of "give to everyone watching" and the leaderboards; their records are kept

See Viewers.

Dashboard

"dashboard": { "macros": ["seq-01m4nqwwap513e991pfg4hfsdx", "seq-01m4nr3cqa2tva4n3aypmpxr1r"] }

The macros pinned as buttons on the Dashboard, in order; at most 50.

Desktop

The desktop app's own preferences, from Settings. Browser mode ignores them.

"desktop": {
  "close_to_tray": true,
  "launch_at_login": true,
  "start_minimized": false,
  "notify": { "live": true, "failures": true, "disconnects": true, "updates": false, "login_warnings_off": false }
}
Key Meaning
close_to_tray Closing the window hides it to the tray instead of quitting
launch_at_login Start CroStream when you log in to the computer (the system keeps the actual registration)
start_minimized Start in the tray with the window hidden
notify.live Notify when the stream goes live
notify.failures Notify when a macro action fails
notify.disconnects Notify when Twitch or OBS loses its connection
notify.updates Notify when a new version is available
notify.login_warnings_off true turns off the warnings about a login expiring or lost (they're on by default)

See The desktop app. Hand edits apply after a reload, except launch_at_login: the operating system keeps that registration, so change it in Settings.

Updates

"updates": { "channel": "beta", "auto_check": false }

channel is stable or beta (which also gets stable releases); left out, it's the channel your copy was released on. auto_check: false turns off the automatic checks; left out, they're on. See Updates.

Diagnostics

"diagnostics": { "reports": true, "tracing": true, "replay": false }

The crash reporting choices. reports turns on sending errors, crashes and logs; tracing and replay turn on performance tracing and session replay, and only count while reports is on. All default to off and are left out of the file while they are. A development build always reports whatever reports says. Hand edits apply after a reload. The environment variables that override this are in Command line.

Export files

Settings → Export & import writes the macros, triggers, queues and webhooks you pick to a JSON file, with what they depend on: a trigger brings its macro and incoming webhook, and macros and triggers bring the queues and webhook destinations they use. Integration settings and secrets are never exported. Webhooks are in a webhooks key shaped like the section above (with exposure and port only when Webhook listener settings was ticked), without tokens, signing keys, destination secrets or sample. Parameters that work like a password (the Discord webhook address) are emptied and listed in blanked, so you know to enter them again after importing.

{
  "format": "crostream.bundle",
  "version": 1,
  "exported_at": "2026-10-09T18:00:00Z",
  "sequences": [],
  "bindings": [],
  "queues": [],
  "blanked": ["macro \"Discord ping\", step \"post\": Webhook address"]
}

Files exported by older versions ("format": "streamlink.bundle") still import. On import:

  • Add as copies (the default) gives everything new IDs and re-links the references between the items. Names already in use get " (2)", " (3)" and so on.
  • Replace matching items overwrites items that have the same ID.

Imported triggers and incoming webhooks always start turned off, and imported webhooks get new tokens. An import never widens exposure. The whole import is checked and saved at once, after one backup. Files over 5 MB, or with more than 500 items of a kind, are refused.

Other files

The config folder holds config.json, backups/, secrets/, overlays/ (one JSON file per overlay) and widgets/ (one per custom widget, with history/ and trash/). The state folder holds what CroStream records:

File What
history.db Events, the activity log and the chat log. Safe to lose: deleting it while CroStream is closed starts history from scratch. Not part of config backups.
viewers.db Viewer records and points. A copy goes to backups/viewers-YYYYMMDD.db in the state folder once a day; the newest 7 are kept.
library/ The media library: library.db (folders, items, tags), files/ (your media, named by item ID) and thumbs/
state/ Live state: counters, timers, waiting queue items, overlay overrides

Back up viewers.db and library/ along with the config folder; they aren't part of the settings backups. The folders' locations on each system are in Where files are kept.