Skip to content
LibertyTalk
Workflows

Workflows in depth

How the engine runs a workflow: triggers, variables, conditions, webhook calls, testing and widgets, with the traps that break a scenario without any error message.

  • Level Advanced
  • 21 min read
  • Updated October 29, 2026
  • Available languages FR · EN

A workflow reacts to an event: a Twitch follow, a member joining your Discord server, a call sent by a third-party service. The engine starts at the trigger and runs the connected nodes one by one, carrying the event's data along. The game integration guides point here for everything about the editor.

Good to know

Workflows are created from the Workflows page of the dashboard. Names in quotes are the labels shown in the editor.

Part 1

The model

One trigger, connected nodes, data flowing through.

One trigger, nodes, data

Every chain starts from a trigger, its entry point. The following nodes are connected by arrows: actions (send a message, call a URL), conditions (pick a branch) and waits. When the event happens, the engine follows the arrows from the trigger and runs each node it reaches.

Data travels with the run. Twitch's “New follower” trigger provides {user_name}, and a Discord “Send a message” placed after it can write {user_name} now follows the channel. Each action adds its own outputs for the next nodes: after a “Webhook / API call”, {webhook_response_status} holds the HTTP code received.

  1. Trigger

    New follower (Twitch)

    provides {user_name}

  2. Action

    Send a message (Discord)

    {user_name} now follows the channel

  3. Action

    Webhook / API call

    POST to your service's URL

  4. Outside LibertyTalk

    Your service

    receives {"user_name":"Alex"}

A Twitch follow announced on Discord, then forwarded to your service.

A node can have several outgoing arrows: each branch runs, one after the other. The graph itself never loops. A node reached a second time through a cycle makes the branch fail, and a node where two paths converge runs only once per trigger. To repeat an action, let the called service do the repeating or chain several nodes.

Choosing the trigger

TriggerWhen it firesExample
Network event: “Member joined”, “New follower”, “Command received”…When the connected network reports the event.Welcome a new Discord member.
“Manual trigger”When you click “Run now” in the editor.Test a chain with “Simulated variables”, without waiting for a real event.
“Incoming webhook”When a third-party service sends a POST to the URL generated for this trigger.A game server reporting the end of a match.
“Scheduled (cron)”At fixed times, from a cron expression. Without a “Timezone”, times are in UTC.0 9 * * 1: every Monday at 9 AM.
“Sub-workflow entry”When another workflow calls it with “Call a workflow”, or when a message button targets it.A “Warn the moderators” routine shared by several workflows.

Tip

Filter at the trigger

Some triggers have a built-in filter. Twitch's “Command received” offers “Filter on command (without !)”: with ticket, only !ticket starts the workflow. The match is exact, lowercase and without the “!”. An empty filter lets everything through.

Tip

A single workflow can hold several triggers, each with its own branch: “Member joined” to welcome people, and a daily “Scheduled (cron)” to post a recap. Each run starts from one trigger only.

Part 2

Variables

What the engine replaces, and what it leaves as is.

Writing a variable

A variable is written between braces: {user_name}. At run time, the engine replaces it with its value in every text field of a node: message, URL, body, headers. Names depend on the trigger: {user_name} for “New follower” (Twitch), {username} for “Member joined” (Discord), {chatter_name} for “Command received”.

In a node's configuration, the “Available variables (Parent trigger)” panel lists what the trigger and previous nodes provide. Click a variable to insert it instead of retyping it: a typo leaves the name verbatim in the result.

FormExampleResult
Simple{bits}The value as is: 500.
Path in an object{payload.user.email}The email field of the user object received by an “Incoming webhook”.
List item{webhook_response.data.0.title}The title of the first item (.0) of the JSON response.
System variable{@date}The server date, in UTC, as YYYY-MM-DD.
System with argument{@discord_relative:joined_at}A relative date that Discord displays as “3 minutes ago”, computed from the joined_at field.
Fill{thumbnail_url | width:1280 height:720}The value of thumbnail_url, in which {width} and {height} become 1280 and 720.

System variables available everywhere: {@now} (ISO date and time), {@date}, {@time} (HH:MM), {@datetime} (DD/MM/YYYY HH:MM), {@timestamp} (Unix timestamp), {@year}, {@month}, {@day}, {@discord_date}, {@discord_relative} and {@uuid}, a different identifier each time it is replaced. Times are the server's, in UTC, except with {@discord_date} and {@discord_relative}: Discord shows the first in each reader's timezone and the second as a relative duration, such as “3 minutes ago”.

What the engine does with your braces

  • An unknown key stays written as is: a typo like {usre_name} shows up verbatim in the sent message. Visible braces in a result almost always point to a misspelled name, or one that belongs to another trigger.
  • A variable declared by the trigger but missing from the event becomes empty: {user_name} on an anonymous Twitch cheer produces nothing.
  • An object or a list is written as JSON, a boolean as true or false.
  • No arithmetic or transformation: {bits / 100} stays written as is, and {user_name | upper} gives the name without uppercasing it. Do the math in the service that receives the call, by sending it the raw value.
  • To accumulate a value over time, “Set community data” in “Add (counter)” mode adds a number to a stored total.

Warning

Double braces from another tool

A template meant for another tool is not protected: in {{user_name}}, LibertyTalk replaces the inside and sends {Alex}. Keep those templates on the side of the tool that reads them, and send it the value in a JSON field.

“Set a variable”

The “Set a variable” action creates a value reusable in the following nodes of the same run. The name is written without braces, using unaccented letters, digits and _: réponse would become rponse. The value can combine other variables. Example: name announcement, value {user_name} just followed the channel, then {announcement} in two messages, one on Discord, the other on Twitch.

Nothing is saved to the database: the variable disappears when the run ends. A value that must survive from one event to the next goes through “Set community data”.

A value made of a single variable holding an object keeps the whole object. With the name reply and the value {webhook_response}, {reply.data.0.title} stays readable further down.

Part 3

Deciding

Conditions, switches and operators.

“If (classic)”

“If (classic)” evaluates one or more conditions and opens two outputs: the green handle for TRUE, the red one for FALSE. Several conditions combine with “All conditions (AND)” or “At least one condition (OR)”, in a single list. To mix both, chain two “If (classic)” nodes.

A condition reads a field, an operator and a value. The field can be written with or without braces (bits or {bits}). The value can contain variables: bits “> greater than” {threshold} compares two values of the run. With no condition at all, the TRUE branch is always taken.

Good to know

A branch left unconnected stops there, without an error. An empty FALSE output simply means “do nothing in that case”.

“Switch”

The “Switch” replaces a cascade of “If” nodes when there are more than two cases. Each case has its own conditions and handle, and the “Else” branch runs when no case matches. In “Exclusive (first match)” mode, only the first matching case is followed. In “All matching (fan-out)” mode, every matching case is.

Example with Twitch's “Channel points reward”: one case per reward, tested on {reward_title}. Each case leads to a “Set a variable” named effect (vip, sound or challenge), then the three branches join a single “Webhook / API call” to https://example.com/hooks/{effect}. One call node instead of three: adding a reward only means adding a case.

  1. Trigger

    Channel points reward

    provides {reward_title}

  2. Condition

    Switch

    one case per reward, plus “Else”

  3. Action

    Set a variable

    effect = vip, sound or challenge

  4. Action

    Webhook / API call

    https://example.com/hooks/{effect}

  5. Outside LibertyTalk

    Your service

    runs the requested effect

A channel points reward forwarded to an external service.

“If network” and “If config”

“If network” compares the network that triggered the run with the one picked in the node (Discord, Twitch, YouTube…), when one chain serves several networks. “If config” reads an answer given when a widget is installed: it is covered in part 6.

Operators and their traps

The editor offers operators based on the variable's type: text, number, yes/no or date. “Incoming webhook” variables, the “Simulated variables” of a “Manual trigger” and the output of “Set a variable” are text for the editor, which therefore doesn't offer numeric comparisons on them.

OperatorWhat it doesTrap
“= equal to”, “≠ different from”Compares the exact text.Alex and alex are different. On a yes/no variable, true, 1, yes and oui all count as TRUE.
“⊃ contains”, “⊄ does not contain”, “⊳ starts with”, “⊲ ends with”Looks for a piece of text.Case-sensitive: “contains” vip won't find VIP. To ignore case, use a regex with i.
“~ matches regex”, “!~ does not match regex”Tests a regular expression.Write the delimiters: /^vip/iu. Without delimiters, a pattern starting with a symbol is misread, and ^vip is always false. i ignores case, u handles accents: /h[ée]ros/iu.
“∅ is empty”, “∃ is not empty”Tests for a missing value.0 counts as empty.
“> greater than”, “≥ greater or equal”, “< less than”, “≤ less or equal”Compares two numbers.If either side is not a number, the condition is false. Only offered for number variables, such as {bits} or {viewers}.
“✓ is true”, “✗ is false”Reads a yes/no variable, such as {webhook_response_ok}.“is true” accepts true, 1, yes and on, but not oui.
Dates: “< before”, “⏪ within the last N days”, “⊙ is today”…Compares dates and periods.“before” and “after” compare by day, ignoring the time.

Warning

A system variable can't be compared directly

A condition's field reads the run's data, not system variables: {@time} used as a field is always empty. Copy it first with “Set a variable” (hour = {@time}), then compare hour. As a value, however, {@date} is replaced as expected.

Part 4

Acting outside LibertyTalk

Outgoing calls, waits and incoming webhooks.

“Webhook / API call”

This action sends an HTTP request: “Destination URL”, “HTTP method” (POST, GET, PUT, PATCH or DELETE), “Body (JSON)” and “Additional headers (JSON)”. Variables work in the URL, the body and the headers. Only public addresses are accepted: localhost or a private network address makes the step fail.

json
{
  "event": "cheer",
  "user_name": "{user_name}",
  "bits": "{bits}",
  "message": "{message}"
}
“Body (JSON)”: every variable in quotes.

Put every variable in quotes, as above. When the body is valid JSON, LibertyTalk escapes the replaced values: a quote typed by a viewer in {message} doesn't break the request. Values are sent as text ("bits":"500"): the receiving service must convert them if it expects numbers.

json
{"Authorization": "Bearer {api_token}"}
“Additional headers (JSON)”: a token provided by a widget variable.

The response feeds the next nodes: {webhook_response_status} (HTTP code), {webhook_response_ok} (true for a 2xx code), {webhook_response_body} (raw body) and {webhook_response}, the JSON response readable by path, for example {webhook_response.data.0.title}. The “JSON path (optional)” field extracts a single value into {webhook_response_value}. The service has 10 seconds to answer.

Warning

A green step doesn't prove the service answered

Service down, timeout, 4xx or 5xx response: the step shows “Completed”, with {webhook_response_ok} false and a code of 0 (no response) or 4xx/5xx. Only a refused address (local or private) and a URL left empty once variables are replaced make the step fail. To react to an outage, add an “If (classic)” on webhook_response_ok “✓ is true”, and connect the FALSE output to “Notify the dashboard”.

  1. Trigger

    Bits received (cheer)

    provides {bits} and {message}

  2. Action

    Webhook / API call

    POST to your service

  3. Condition

    If (classic)

    webhook_response_ok ✓ is true

  4. Action

    Notify the dashboard

    on the FALSE branch: “Service unreachable”

Detecting a service that is offline.

“Webhook / API call (background)” sends the same request from a queue: the request doesn't block the engine, and the next nodes resume once the response arrives. Meanwhile, the step shows “Waiting”. It only provides {webhook_response_status}, {webhook_response_body} and {webhook_response_ok}.

The “Sign the request” option adds the X-LibertyTalk-Signature header, computed with a key that belongs to LibertyTalk: it is meant for LibertyTalk tools such as LibertyBroad, and a third-party service cannot verify it. To authenticate your calls to your own service, put a token in the headers instead.

“Wait”

“Wait” pauses the branch for a “Duration” and a “Unit” (seconds, minutes, hours or days). The maximum is 7 days: a longer duration is brought back to 7 days. The wait blocks nothing else: other branches keep going, and the step shows “Waiting” until it resumes. Example: a welcome message, “Wait” 10 minutes, then a second message with the server rules.

If the workflow is edited during a wait, the resume fails instead of running a graph that changed in the meantime.

“Incoming webhook”

The “Incoming webhook” trigger gives your workflow an address. The URL and the secret appear in the “Webhook URL & secret” field after the first save. A third-party service uses them to send a POST with a JSON body, and each accepted call starts a run.

The secret goes in the X-Webhook-Secret header. The “Secret header” field changes that name when the sender imposes its own, for example X-Gitlab-Token for GitLab. Don't put the secret in the URL (?secret=): it would end up in plain text in server logs.

bash
curl -X POST "<webhook-url>" \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Secret: <webhook-secret>" \
  -d '{"player":"Alex","score":42}'
Testing the URL from a terminal: replace the two values in angle brackets with the trigger's.

LibertyTalk answers 202 when the call is accepted, 401 if the secret or signature is wrong, 404 if the URL is unknown or the workflow is inactive, 400 if a body announced as JSON can't be read, and 429 beyond 120 accepted calls per minute.

For stronger protection, sign the request. Send the Unix timestamp in seconds in X-LT-Timestamp, and in X-LT-Signature the value sha256= followed by the HMAC-SHA256 of {timestamp}.{body}, computed with the secret. LibertyTalk rejects a timestamp more than 5 minutes off and a signature it has already received: an intercepted request can't be replayed.

send-event.mjsjavascript
import { createHmac } from 'node:crypto'

const url = process.env.LT_WEBHOOK_URL
const secret = process.env.LT_WEBHOOK_SECRET

const body = JSON.stringify({ player: 'Alex', score: 42 })
const timestamp = Math.floor(Date.now() / 1000).toString()
const signature = createHmac('sha256', secret)
  .update(`${timestamp}.${body}`)
  .digest('hex')

const res = await fetch(url, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-LT-Timestamp': timestamp,
    'X-LT-Signature': `sha256=${signature}`,
  },
  body,
})
console.log(res.status)
Sending a signed event from Node.js 18 or later.

In the workflow, {payload} holds the whole received body and {payload.score} a single field. The “Variables from the payload” field creates short names: “Target variable” score, “Key in the received payload” score, and {score} becomes usable in nodes and conditions alike. These variables are text for the editor: rather than a score to compare with a threshold, have the service send a value ready for “= equal to”, for example "tier":"gold".

In a widget, each installation gets its own URL and secret, shown on the installation page.

Warning

No loops through webhooks

A workflow that calls its own “Incoming webhook” URL is refused at save time. Across several workflows, each bounce is counted by the X-LibertyTalk-Hop header, and the chain is cut at the fifth bounce.

Part 5

Testing and debugging

Activate, test without a live event, read the execution console.

Save, activate, test

Saving stores the graph. For network events, schedules and incoming webhooks to reach the workflow, it must also be active. On activation, LibertyTalk checks the platform rules: a sequence a platform forbids blocks the activation. The Platform rules page details each rule.

The “Manual trigger” runs even when the workflow is inactive: it's the way to test without waiting for a real event. Add it next to the real trigger, fill in its “Simulated variables” (user_name = Test, bits = 500), connect it to the first node of the chain and click “Run now”. The run uses the saved version: “Save and run” avoids testing an outdated one.

Warning

A test has real effects: the bot actually posts, assigns roles, calls your URLs. Test on a Discord server or a test channel before wiring your community.

Reading the execution console

The “Execution console”, below the editor, shows every run step by step, trigger included. Click its header to open it: runs show up live, and “Load last run” reloads the most recent one. Each step shows its “Input” (values received, variables replaced) and its “Output” (variables produced). For a condition, it shows the compared values and the branch taken.

StatusWhat it means
CompletedThe step succeeded. For a “Webhook / API call”, also check webhook_response_ok.
Failed, continuedThe action failed but the chain went on. Open the step to read the error.
FailedThe branch stopped there: configuration error, platform rule, cycle or limit reached.
WaitingA “Wait” or a background call hasn't finished yet.
AbortedThe run was force-stopped, for example because the workflow was deactivated while it was running.

On each step, the curly-brace “Variables” icon shows every variable available at that point, with secrets masked. That's where you find out why a token came out empty or unchanged: you read the exact name of each key and its value.

Common mistakes

My message shows {user_name} instead of the name.
The key doesn't exist in this run: a typo, or a name that belongs to another trigger ({username} on “Member joined”, {user_name} on “New follower”). Open “Variables” on the step, in the console, to read the real names.
My “contains” condition never matches.
It is case-sensitive. Replace it with “~ matches regex” and /word/iu.
My regex is always false.
The delimiters are probably missing: ^vip is misread, /^vip/ works.
The webhook call is green but my service received nothing.
A network error, a timeout or a 4xx/5xx response leave the step “Completed”. Read webhook_response_status in the step's output: 0 means the request never went through.
My webhook call fails with “Webhook URL is empty”.
The URL is empty once variables are replaced, often because a widget variable such as {service_url} wasn't filled in at install time. Open “Variables” on the step to read its value.
My incoming webhook answers 404.
The workflow isn't active, or the URL was copied wrong. Activate it, then copy the URL again from “Webhook URL & secret”. A URL shut off after repeatedly exceeding 120 calls per minute comes back when you save the workflow again.
The manual test doesn't use my latest change.
The run starts from the saved version. Use “Save and run”.
“> greater than” doesn't appear in the operator list.
For the editor, the variable is text (incoming webhook, simulated variables, “Set a variable”). Compare a value prepared by the service with “= equal to”, or use a regex.

Limits to know

  • No loops: a cycle in the graph makes the branch fail, and a chain stops after 100 nodes in a row.
  • At most 50 calls per trigger, counting “Webhook / API call”, its background version and “Call a workflow”, sub-workflows included. Beyond that, the step fails.
  • More than 5 HTTP calls or sub-workflow launches reachable from one trigger: the editor shows a warning at save time, without blocking.
  • “Wait”: 7 days at most.
  • “Call a workflow”: at most 5 nested call levels. A workflow can call another “Sub-workflow entry” it contains, but a routine can't call itself again: calling an entry already running in the chain fails.
  • “Incoming webhook”: 120 accepted calls per minute. After 3 minutes over the limit within 10 minutes, the URL is disabled until the workflow is saved again.
  • No native arithmetic, random draw or cooldown: leave them to the called service.
Part 6

Reusing and sharing

Sub-workflows, widgets and the Clu assistant.

Sub-workflows

A “Sub-workflow entry” makes a chain callable, like a function. Give it an “Entry name” and declare its “Expected parameters”, each with an optional default value: they become variables further down. One workflow can hold several entries.

The “Call a workflow” action picks the “Routine to call” and fills in the “Parameters passed”. With “Wait for the result” set to Yes, the rest of the chain gets the variables the routine produced, but the routine can contain neither a “Wait” nor a background call. Set to No, the routine runs in the background and nothing comes back.

Example: a “Warn the moderators” routine with a reason parameter, called by the anti-spam workflow and by the reports workflow. The alert message is edited in one place.

From workflow to widget

A widget is a group of nodes that can be installed in a community, with settings specific to each installation. Click “New widget” in the editor controls (or type “widget” in the “+” palette), put the trigger and actions inside the frame, then open its gear icon to give it a name, a category and its variables. A regular zone becomes a widget when you tick “Make this group an installable widget”. The trigger must sit inside the frame: triggers are what set the widget's compatible networks.

In “Widget variables”, declare what the installer must provide, for example service_url and api_token. These names are then used like any variable: {service_url}/events in a “Webhook / API call” URL, {api_token} in its headers. On a node field, the “Ask at install time” box lets the installer choose the value, yours being the default.

“If config” sets a “Question asked at install time”, for example “Enable daily stats?”, with a Yes / No, Text, Number or Choice answer. The TRUE branch runs when the node's comparison holds: each community turns a part of the widget on or off that way.

Installing a widget freezes a copy of it. After a change, the installation page offers an update: webhook URLs and secrets are kept, only settings tied to deleted nodes can be lost.

  1. Trigger

    Member joined (Discord)

    provides {user_mention}

  2. Action

    Send a message (Discord)

    Welcome {user_mention}!

  3. Condition

    If config

    “Enable daily stats?”

  4. Action

    Record a stat

    one counter per day, for example daily_join_{@date}

A welcome widget with a statistics option.

Clu, the editor assistant

The “Clu” button in the editor opens an assistant. Describe the scenario in one sentence, for example “When someone joins the Discord, send them a welcome message”: Clu asks questions if needed, then proposes nodes that you add with “Apply”. Review them before saving, especially networks and variable names. Clu needs an AI key, saved in Settings, “IA User” or “IA Account” tab.

Going further

Try it on your own community

Create a free account and follow the guide with your own networks.

Ask on Discord