Welcome to the official user guide for MacroHero β the all-in-one macro and automation extension for Owlbear Rodeo. Build visual controls, automate your table and connect your favorite extensions.
MacroHero uses pure JavaScript for all expressions and commands. Variables are referenced directly by name (no special syntax), and you have full access to JavaScript operators, functions, and control structures. All integration functions are async β use await when calling them in commands.
https://macrohero.onrender.com/manifest.json
using Owlbear Rodeo's "Install from manifest" option.expression. Optionally set numeric min/max.await when calling integration functions (e.g., await OwlTrackers.addValue(token, 'HP', 10)), reference variables by name, use operators, conditionals, and any standard JavaScript syntax.min/max and accept an optional step setting (default 1).The configuration modal includes a Token Viewer tab that displays all tokens and items currently in your Owlbear Rodeo scene. This tool helps you:
"token": { "expression": "'copied-token-id'" }Usage: Open the config modal (βοΈ) and click the Token Viewer tab. Use the search box to find tokens by name, or expand layers to browse all items. Click "Copy ID" next to any token to use it in your configuration.
MacroHero includes a comprehensive Debug Mode system that lets you enable detailed console logging for specific modules. This is invaluable for troubleshooting expressions, commands, and integration calls.
Usage: Open the config modal (βοΈ), click the Debug Mode tab, and check the boxes for modules you want to debug. Open your browser's developer console (F12) to see the logs. Useful for diagnosing why expressions return unexpected values or why commands fail.
Each page's layout array is built from items. Below is the complete reference for every available item type and its properties.
text: Title text (supports ${var} expressions)color: Optional custom color{ "type": "title", "text": "Section Name" }${varName} to embed variable values.
content / text: Content string (Markdown auto-detected){ "type": "text", "content": "HP is ${hp}" }color: Line color (default: accent)height: Thickness (default: "1px")margin: Spacing above/below (default: "12px")style: "solid" | "dashed" | "dotted"{ "type": "divider" }type / alert: "info" | "warning" | "success" | "error"title: Optional headingtext / message: Alert body text{ "type": "alert", "alert": "warning", "title": "Warning", "text": "HP is low!" }var: Variable namelabel: Display labelcolor: Optional accent color{ "type": "value", "var": "hp", "label": "HP" }var: Variable namelabel: Display labelplaceholder: Placeholder textonupdate: Commands array executed on change{ "type": "input", "var": "note", "label": "Note", "placeholder": "Enter text..." }var: Variable name (boolean)label: Display labelonupdate: Commands array executed on change{ "type": "checkbox", "var": "isActive", "label": "Active?" }var: Variable name (boolean)label: Display labelcolor: Optional accent coloronupdate: Commands array executed on change{ "type": "toggle", "var": "isEnabled", "label": "Enabled" }var: Variable namelabel: Display labelstep: Increment amount (default: 1)min / max: Overrides variable bounds if setonupdate: Commands array executed on change{ "type": "counter", "var": "revives", "label": "Revives", "step": 1 }var: Variable namelabel: Display labeloptions: Array of strings or {value, label} objectsoptionsVar: Variable name containing a dynamic options arrayonupdate: Commands array executed on change{ "type": "dropdown", "var": "mode", "label": "Mode", "options": ["Attack", "Defend", "Heal"] }label: Button text (supports ${var} expressions)onclick: JavaScript script executed on left-click (string, or array of strings joined with newlines)onrightclick: JavaScript script executed on right-clicktooltip: Hover tooltip textcolor: Custom button color{ "type": "button", "label": "Heal 10", "onclick": ["await OwlTrackers.addValue(token, 'HP', 10)"], "tooltip": "Restore 10 HP" }columns: Number of columns (default: 4)buttonSize: Button size (default: "40px")gap: Gap between buttons (default: "4px")buttonShape: "rectangle" or "square" (default)border: Boolean β add border stylingchildren: Array of button configs, each with label, icon, color, borderColor, onclick, onrightclick, tooltip{ "type": "matrix", "columns": 3, "children": [{"icon": "βοΈ", "tooltip": "Attack", "onclick": ["..."]}] }{ "type": "row", "children": [ ... ] }children: Array of child itemsborder: Boolean β add bordercolor: Accent color{ "type": "stack", "border": true, "children": [ ... ] }MacroHero is configured using a JSON structure that defines global variables, pages, UI layout, and commands. You can edit this configuration via the visual editor (βοΈ) or by importing/exporting JSON. Below is a step-by-step guide with copy-pasteable examples for every feature.
global SectionThe global section sets up your extension's title, window size, and variables available everywhere. Hereβs a real-world example:
{
"global": {
"title": "Macro Hero",
"width": 800,
"height": 600,
"variables": {
"playerName": {
"expression": "Local.value('playerName', 'Unknown')"
}
}
}
}
expression (see below for syntax).Variables are defined in the variables section of either global or a page. Each variable uses an expression string written in pure JavaScript. You can reference other variables directly by name, use standard JavaScript math operators, call integration functions, and use any JavaScript syntax (ternary operators, string concatenation, etc.).
{
"variables": {
"hp": { "expression": "OwlTrackers.getValue(token, 'HP')" },
"isBandaged": { "expression": "ConditionMarkers.hasCondition(token, 'Bandaged')" },
"bonus": { "expression": "5" },
"total": { "expression": "hp + bonus" }
}
}
GoogleSheets.getValue, Local.value, OwlTrackers.getValue, ConditionMarkers.hasCondition, etc.(hp > 10 ? 'Healthy' : 'Wounded')min and max numeric properties. These are used by counter UI items (see below) to constrain the input range.Each page's layout array defines the UI. See the Items Reference for the full list of types and properties. Quick reference:
{ "type": "title", "text": "Section Name" }${var} expressions. { "type": "text", "content": "HP is ${hp}" }{ "type": "divider" }{ "type": "alert", "alert": "info", "text": "Message" }{ "type": "value", "var": "hp", "label": "HP" }{ "type": "input", "var": "note", "label": "Note" }{ "type": "checkbox", "var": "isActive", "label": "Active?" }{ "type": "toggle", "var": "isEnabled", "label": "Enabled" }step (default 1) sets the increment; variable min/max constrain the range.{ "type": "counter", "var": "revives", "label": "Revives", "step": 1 }{ "type": "dropdown", "var": "mode", "label": "Mode", "options": ["Attack", "Defend"] }onclick is an array of strings forming a JavaScript script. Supports onrightclick and tooltip.{ "type": "button", "label": "Heal 10", "onclick": ["await OwlTrackers.addValue(token, 'HP', 10)"] }{ "type": "matrix", "columns": 3, "children": [{"icon": "βοΈ", "onclick": ["..."]}] }{ "type": "row", "children": [ ... ] }{ "type": "stack", "children": [ ... ] }The onclick, onrightclick, and onupdate fields accept a JavaScript script as an array of strings β each string is a line, joined and executed in an async context. Variables are in scope by name; use await for async integration calls.
{
"type": "button",
"label": "Heal if Bandaged",
"onclick": ["await (isBandaged ? OwlTrackers.addValue(token, 'HP', 10) : OwlTrackers.addValue(token, 'HP', -5))"]
}
Supported integrations include: GoogleSheets, Local, OwlTrackers, StatBubbles, ConditionMarkers, ColoredRings, PrettySordid, JustDices, DicePlus, Weather, Embers, Auras, Aurora, Announcement. See the API reference for all functions.
{
"global": { ... },
"pages": [
{
"label": "Page Name",
"variables": { ... },
"layout": [ ... ]
}
]
}
MacroHero stores different kinds of data in different places to balance persistence, size limits, and privacy:
com.sewef.macrohero/fullConfig/<roomId>). This is written when you save in the configuration modal and on automatic saves.com.sewef.macrohero/playerConfigs, namespaced by player ID. This persists across reloads and is shared within the room (but namespaced to avoid collisions).macrohero.gsheet.apiKey and macrohero.gsheet.sheetId. The config modal enforces that credentials are provided when the configuration references Google Sheets.The full configuration JSON is stored in your browser localStorage. It can be lost if you clear site data, switch browsers/devices, or if localStorage is unavailable. Please export or copy your configuration from the JSON editor and save it to external storage (cloud drive, local backup, etc.).
{
"label": "Health Controls",
"variables": {
"token": { "expression": "'token-id-here'" },
"hp": { "expression": "OwlTrackers.getValue(token, 'HP')" },
"maxHp": { "expression": "OwlTrackers.getMax(token, 'HP')" },
"revives": { "expression": "3", "min": 0, "max": 10 }
},
"layout": [
{ "type": "title", "text": "HP Controls" },
{ "type": "value", "var": "hp", "label": "Current HP" },
{ "type": "value", "var": "maxHp", "label": "Max HP" },
{ "type": "button", "label": "Heal 10", "onclick": ["await OwlTrackers.addValue(token, 'HP', 10)"] },
{ "type": "button", "label": "Damage 5", "onclick": ["await OwlTrackers.addValue(token, 'HP', -5)"] },
{ "type": "button", "label": "Heal to Max", "onclick": ["await OwlTrackers.setValue(token, 'HP', maxHp)"] },
{ "type": "title", "text": "Revives" },
{ "type": "value", "var": "revives", "label": "Revives Left" },
{ "type": "counter", "var": "revives", "label": "Use Revive", "step": 1 }
]
}
Notes: the revives variable includes min/max so the counter will stay within 0β10, and step is set to 1 (optional).
Paste this into your configuration (via the visual editor or JSON import) and adjust variable names as needed.
MacroHero provides integrations with popular Owlbear Rodeo extensions and external services. All integration functions are async and should be used with await in commands when necessary.
See the detailed API reference below for all available functions.
This section documents all public functions exposed by MacroHero's built-in integrations. Use these functions in variable expressions and in commands. Examples show how to call them from configuration JSON (expressions) or commands.
Local.value('playerName', 'Unknown')
Local.set('playerName', 'Xalithra')
GoogleSheets.getValue('Sheet1', 'A1') (returns a single cell when possible).GoogleSheets.getRange('Sheet1', 'A1:B4'). Returns a 2D array (or a flattened 1D array for single-column ranges).JustDices.roll('1d20+5')trackerConfig: {variant, name?, color?, value?, max?, ...}. Returns the new tracker ID.
For full API support, you must use either ConditionMarkers-API (manifest), which is identical but exposes a dedicated API, or ConditionMarkers-Extended (manifest), which adds the API and enables numeric value support for conditions.
The default ConditionMarkers extension does not provide API access or numeric value features. The two extensions above have been built to be fully compatible with MacroHero's ConditionMarkers integration.
Embers provides a fluent API for creating and chaining spell visual effects (projectiles, area effects, cone effects, and gameplay actions) using the Embers extension for Owlbear Rodeo.
await Embers.sequence().projectile('effectId', caster, target).cast()effectId (string), casterId (token id), targetIds (token id or array), config (optional: size, spellName, etc.)
.projectile('generic.weapon_attacks.ranged.laser_shot.orange', caster, target, {spellName: 'Fireball'})effectId (string), tokenIds (token id or array), config (optional: size, spellName, etc.)
.aoe('generic.impact.fire', targetToken, {size: 5, spellName: 'Explosion'})effectId (string), casterId (token id), targetId (token id), config (optional: size, spellName, etc.)
.cone('generic.weapon_attacks.melee.single.generic_slash', caster, target, {size: 6, spellName: 'Cone Attack'})actionId (string), args (object), config (optional configuration)
.action('move', {target: targetId, distance: 10}).delay(300) delays the next effect by 300ms..projectile(...).then().aoe(...) β projectile fires first, then AOE executes after.options (optional: spellName, destination for broadcast, etc.)
await Embers.sequence().projectile(...).cast().named('Fireball').withOptions({spellName: 'Lightning Bolt'})await Embers.castProjectile('effectId', caster, target, {spellName: 'My Spell'})await Embers.castAOE('effectId', target, {size: 5})await Embers.castCone('effectId', caster, target, {size: 6})Single Projectile Effect
await Embers.sequence()
.projectile('generic.weapon_attacks.ranged.laser_shot.orange', caster, target)
.cast()
Projectile Followed by AOE (Sequential)
await Embers.sequence()
.projectile('generic.weapon_attacks.ranged.laser_shot.orange', caster, target, {spellName: 'Fireball'})
.then()
.aoe('generic.impact.fire', target, {size: 5})
.cast()
Multiple Projectiles (Simultaneous)
await Embers.sequence()
.projectile('effectId', caster, target1)
.projectile('effectId', caster, target2)
.projectile('effectId', caster, target3)
.cast()
Cone Effect with Delay
await Embers.sequence()
.cone('generic.weapon_attacks.melee.single.generic_slash', caster, target, {size: 6, spellName: 'Cone Attack'})
.named('Sweep')
.cast()
Complex Chain with Delays
await Embers.sequence()
.projectile('missile', caster, target, {spellName: 'Chain Lightning'})
.delay(200)
.then()
.aoe('impact', target, {size: 4})
.delay(300)
.then()
.cone('secondary', caster, target2, {size: 3})
.cast()
Using Convenience Helpers
// Shortcut for simple projectiles
await Embers.castProjectile('effectId', caster, target, {spellName: 'Arrow'})
// Shortcut for AOE
await Embers.castAOE('effectId', target, {size: 5})
// Shortcut for cone
await Embers.castCone('effectId', caster, target, {size: 6})
When you call .delay(ms), the delay is applied to the next effect, not the previous one. Delays stack β multiple consecutive .delay() calls add together.
// This delays the aoe by 300ms (projectile executes, then 300ms passes, then aoe fires)
Embers.sequence()
.projectile('effect1', caster, target)
.delay(300)
.aoe('effect2', target)
.cast()
The .then() method creates a nested sequence where following effects execute after all previous effects complete. Without .then(), effects execute simultaneously. You can chain multiple .then() calls to create a complex sequence.
// Effects execute in order: projectile β (waits for completion) β aoe β (waits) β cone
await Embers.sequence()
.projectile('missile', caster, target)
.then()
.aoe('impact', target, {size: 5})
.then()
.cone('secondary', caster, target2)
.cast()
await when calling .cast() to ensure effects execute before the next command..cast() return the EmbersSequence instance for chaining..cast() is called, not when methods are chained, allowing dynamic positioning.Control weather effects on maps. Works with the official Weather extension. For advanced features (ENERGYSTORM, WATER, CURRENT weather types and tint color support), use the weather-extended fork.
{type, speed?, density?, direction?, tint?}
tint parameter requires weather-extended
Weather.setWeather(mapId, {type: 'SNOW', speed: 2, density: 3})Weather.updateWeather(mapId, {tint: '#88aaff'})Manage auras on tokens and items using the Auras extension (formerly Emanation). Auras are visual effects that appear around tokens, including glows, bubbles, ranges, images, and custom effects.
const hasAuras = await Auras.hasAura(token)const auras = await Auras.getAuras(token)imageBuildParams)sksl parameter){image: ImageContent, grid: ImageGrid}await Auras.addAura(token, { style: 'Glow', color: '#FF0000', size: 5 })await Auras.removeAura(token)Add a Red Glow Aura
await Auras.addAura(token, {
style: 'Glow',
color: '#FF0000',
size: 5
})
Add a Blue Range Aura with Opacity
await Auras.addAura(token, {
style: 'Range',
color: '#0000FF',
size: 8,
opacity: 0.6
})
Add an Aura with Blend Mode (Glow + PLUS mode simulates light)
await Auras.addAura(token, {
style: 'Glow',
color: '#FFFF00',
size: 4,
blendMode: 'PLUS'
})
Add a Custom Shader Aura
await Auras.addAura(token, {
style: 'Custom',
size: 5,
sksl: 'your_shader_code_here'
})
Check and Remove Auras
const hasAuras = await Auras.hasAura(token)
if (hasAuras) {
const aurasList = await Auras.getAuras(token)
console.log(`Token has ${aurasList.length} aura(s)`)
await Auras.removeAura(token)
}
imageBuildParams object with ImageContent and ImageGrid properties. These are advanced primitives from the OBR SDK; for simpler image auras, use the Auras extension UI directly.style, color, or opacity, the extension uses the current player's default settings. This is useful for creating auras that respect player preferences.hasAura() and getAuras() functions inspect Auras extension metadata. To modify auras, always use addAura() and removeAura() β do not edit metadata directly.Advanced utility functions for scene and token manipulation. These are helper modules exposed directly in the expression/command context.
tokenId. Optional filter: layer name, array of layers, or {layers?, excludeOverlapping?}.url (image URL, "SELECT" to open asset picker, or "EMPTY"), position ("HERE" = viewport center or {x, y}), scale, layer, name, visible, locked.Low-level OBR SDK access for expert users. These modules provide direct metadata manipulation.
These expose raw OBR SDK operations. Use with caution. Refer to source code for function signatures.
Control ambient lighting and time-of-day effects on maps using the Aurora extension.
{saturation?, lightness?, hue?, opacity?, blendMode?, feather?, dreamy?}
await Aurora.setAurora(mapId, {lightness: -0.4, hue: 220})MIDNIGHT, GOLDEN_HOUR, PRE_DAWN, BLOOD_MOON.
await Aurora.setAurora(mapId, Aurora.getPresets().MIDNIGHT)Advanced dice rolling with full dice notation support: exploding dice (!), keep highest (kh), drop lowest (dl), and more.
2d20kh1, 4d6dl1, 3d6!.
{rollTarget?, showResults?, timeoutMs?}
await DicePlus.roll('2d20kh1')const total = await DicePlus.rollTotal('4d6dl1')Show and manage scene-wide announcement banners visible to all players.
active defaults to true.
await Announcement.setAnnouncement('Round 3 begins!', true){content?, active?}.Here is a full, copy-pasteable page example that demonstrates variables, expressions, counters with min/max, inputs, Weather integration, and commands that call integrations (GoogleSheets, OwlTrackers, JustDices).
{
"label": "Character Demo",
"variables": {
"token": { "expression": "'your-token-id-here'" },
"hp": { "expression": "OwlTrackers.getValue(token, 'HP')" },
"maxHp": { "expression": "OwlTrackers.getMax(token, 'HP')" },
"name": { "expression": "GoogleSheets.getValue('Roster', 'A2')" },
"mapId": { "expression": "sceneHelpers.getMapIdFromToken(token)" },
"revives": { "expression": "3", "min": 0, "max": 5 }
},
"layout": [
{ "type": "title", "text": "Character Demo" },
{ "type": "value", "var": "name", "label": "Name" },
{ "type": "row", "children": [
{ "type": "value", "var": "hp", "label": "HP" },
{ "type": "value", "var": "maxHp", "label": "Max" }
]},
{ "type": "row", "children": [
{ "type": "button", "label": "Heal 10", "onclick": ["await OwlTrackers.addValue(token, 'HP', 10)"] },
{ "type": "button", "label": "Damage 5", "onclick": ["await OwlTrackers.addValue(token, 'HP', -5)"] }
]},
{ "type": "divider" },
{ "type": "title", "text": "Weather Controls" },
{ "type": "row", "children": [
{ "type": "button", "label": "Snow", "onclick": ["await Weather.setWeather(mapId, {type: 'SNOW', speed: 2, density: 3})"] },
{ "type": "button", "label": "Rain", "onclick": ["await Weather.setWeather(mapId, {type: 'RAIN', speed: 3, density: 4})"] },
{ "type": "button", "label": "Remove", "onclick": ["await Weather.removeWeather(mapId)"] }
]},
{ "type": "divider" },
{ "type": "value", "var": "revives", "label": "Revives" },
{ "type": "counter", "var": "revives", "label": "Use Revive", "step": 1 },
{ "type": "input", "var": "note", "label": "Note", "placeholder": "Notes..." },
{ "type": "button", "label": "Quick Attack", "onclick": ["await JustDices.roll('1d20 + ' + (hp % 5))"] }
]
}
Paste this page JSON into the editor (or import it). Replace 'your-token-id-here' with a real token id. The revives counter respects min/max, step sets the increment, and weather buttons demonstrate the Weather integration.
Use stack to group items vertically. Here's a minimal example with a multi-line button script:
{
"label": "Stack + MultiCmd",
"variables": {
"token": { "expression": "'your-token-id-here'" },
"revives": { "expression": "2", "min": 0, "max": 5 }
},
"layout": [
{ "type": "title", "text": "Stack & Multi-Command" },
{ "type": "stack", "children": [
{ "type": "value", "var": "revives", "label": "Revives Left" },
{ "type": "input", "var": "note", "label": "Note" },
{ "type": "button", "label": "Use Revive & Roll",
"onclick": [
"revives = revives - 1",
"await OwlTrackers.addValue(token, 'HP', 10)",
"await JustDices.roll('1d6')"
]
}
]}
]
}
When the button is clicked it decrements revives, heals the token's HP by 10, then rolls a d6 and returns the result.
Advanced usage focuses on combining variables, expressions and integrations to build responsive and robust UI pages.
variables not in button commands.min/max to numeric variables when you display them with counter items.Number(), floor()) when mixing strings and numbers._resolved values in your exported config to inspect resolved results.value items to surface intermediate variables for debugging.await when calling integration functions.setValue() and addValue() inside commands to modify page variables safely β those changes will trigger dependent variable recalculation.GoogleSheets.getValue) instead of calling low-level APIs yourself.button that runs a simple diagnostic command (e.g., JustDices.roll('1d6')).GoogleSheets.getValue('Sheet', 'A1').undefined or cause errors; ensure all referenced variables are defined.token variable.Need help, found a bug, or want to suggest an improvement? Choose whichever channel works best for you.
@citron3400
Join server
GitHub Issues
Report a reproducible bug or request a feature.
Open an issue