When a Kick stream goes live, your audience may be spread across Bluesky, Discord, and Telegram. You're going to build a single rule that:
- Watches Kick for stream-live events.
- Posts a Bluesky note announcing the stream.
- Pings a Discord channel.
- DMs a Telegram subscriber list.
This is the multi-connector fan-out shape — one trigger, N parallel actions. No agents, no cooperation, just a wide rule.
Time: 30 minutes (longer if you have to set up Bluesky / Discord / Telegram from scratch).
You'll need:
- Springtale installed and
springtale initrun. - A Kick account with at least one stream you can start (or pick a
channel you don't own and use the
channel_followedtrigger instead — same shape, different trigger). - A Bluesky account.
- A Discord server you can install a bot in.
- A Telegram bot (see tutorial 01 for setup).
We need four connectors. Set their credentials in the vault:
# Kick: OAuth 2.1 PKCE flow — the CLI walks you through it.
springtale-cli connector setup kick
# Bluesky: ATProto session — needs handle + app password.
# Create an app password at https://bsky.app/settings/app-passwords
springtale-cli vault set bluesky.handle # your.handle.bsky.social
springtale-cli vault set bluesky.app_password
# Discord: bot token from https://discord.com/developers/applications
springtale-cli vault set discord.bot_token
# Telegram: from BotFather, same as tutorial 01
springtale-cli vault set telegram.bot_tokenEach vault set prompts for the value from stdin. Tokens never go
through argv or env.
Verify all four loaded:
springtale-cli server start &
sleep 2
springtale-cli connector listYou should see four connectors with status Active.
Save as rules/stream-live-fanout.toml:
[rule]
name = "stream-live-fanout"
description = "When my Kick stream goes live, announce on Bluesky + Discord + Telegram."
enabled = true
[trigger]
type = "ConnectorEvent"
connector = "connector-kick"
event = "stream_live"
# Optional: only fire for *your* channel. The trigger payload
# includes broadcaster.username; filter on it so you don't get
# pinged for everyone you follow going live.
[[conditions]]
type = "FieldEquals"
field = "trigger.broadcaster.username"
value = "your_kick_username"
# Bluesky post.
[[actions]]
type = "RunConnector"
connector = "connector-bluesky"
action = "create_post"
[actions.params]
text = "🔴 LIVE on Kick — ${trigger.title}\n\nhttps://kick.com/${trigger.broadcaster.username}"
# Discord ping.
[[actions]]
type = "RunConnector"
connector = "connector-discord"
action = "send_message"
[actions.params]
channel_id = "YOUR_DISCORD_CHANNEL_ID"
text = "@everyone — going live now: ${trigger.title}\nhttps://kick.com/${trigger.broadcaster.username}"
# Telegram DM. For a real subscriber list, use Chain to iterate.
# For one DM (or a Telegram channel), just hardcode the chat_id.
[[actions]]
type = "RunConnector"
connector = "connector-telegram"
action = "send_message"
[actions.params]
chat_id = "YOUR_TELEGRAM_CHANNEL_ID"
text = "🔴 LIVE on Kick — ${trigger.title}\n\nhttps://kick.com/${trigger.broadcaster.username}"Replace the channel IDs / username with yours, then load:
springtale-cli rule add rules/stream-live-fanout.tomlTwo ways:
Real test: Start a stream on Kick. Within a few seconds you should see the post on Bluesky, the ping in Discord, the message in Telegram.
Synthetic test: springtale-cli rule run stream-live-fanout
fires the rule against a synthetic payload, which lets you verify
the action dispatches succeed without actually streaming. The
synthetic payload has placeholder values; check the dispatched
content shows the placeholder text.
Inspect what happened:
springtale-cli events --limit 10
springtale-cli trace --rule stream-live-fanouttrace is real-time. Keep it open while you stream.
Kick's stream_live trigger fires every time the stream toggles to
live. If your stream drops and reconnects, you'll fan out twice.
Add a cooldown via session memory:
# Append to rules/stream-live-fanout.toml, at the top of [[actions]]:
[[conditions]]
type = "And"
[[conditions.children]]
type = "FieldEquals"
field = "trigger.broadcaster.username"
value = "your_kick_username"
[[conditions.children]]
type = "SessionFieldOlderThan"
session_key = "kick-live-cooldown"
field = "last_fanout_at"
duration = "30m"And add a session-write action before the platform fan-outs:
[[actions]]
type = "Transform"
op = "SetSessionField"
[actions.params]
session_key = "kick-live-cooldown"
field = "last_fanout_at"
value = "${now}"Now if Kick fires stream_live twice within 30 minutes, the second
firing finds last_fanout_at < 30 minutes old and the condition
short-circuits. Fan-out happens once per stream session.
You probably want each platform's message to read differently:
- Bluesky: 300-character limit, hashtag-friendly.
- Discord: rich embed, image, link preview.
- Telegram: long-form OK, supports Markdown.
Switch from a single text field to per-action formatting:
# Bluesky — short, hashtag-friendly:
[actions.params]
text = "🔴 Live: ${trigger.title} — kick.com/${trigger.broadcaster.username} #${trigger.category}"
# Discord — embed:
[[actions]]
type = "RunConnector"
connector = "connector-discord"
action = "send_embed"
[actions.params]
channel_id = "YOUR_DISCORD_CHANNEL_ID"
title = "${trigger.title}"
description = "${trigger.broadcaster.username} is live on Kick"
url = "https://kick.com/${trigger.broadcaster.username}"
color = 0xFF0000
image = "${trigger.thumbnail_url}"
# Telegram — Markdown:
[[actions]]
type = "RunConnector"
connector = "connector-telegram"
action = "send_message"
[actions.params]
chat_id = "YOUR_TELEGRAM_CHANNEL_ID"
text = "🔴 *${trigger.title}*\n\n[Watch on Kick](https://kick.com/${trigger.broadcaster.username})"
parse_mode = "Markdown"| Concept | Where it appeared |
|---|---|
OAuth setup via connector setup |
Step 1 — Kick |
| Multi-connector single rule | Step 2 — three platform actions |
| Trigger payload filtering | Step 2 — FieldEquals on broadcaster |
| Variable interpolation across services | Step 2 — ${trigger.title} everywhere |
| Synthetic rule runs | Step 3 — rule run … |
| Idempotency via session memory | Step 4 — cooldown |
| Per-platform formatting | Step 5 — embed vs Markdown vs plain |
| Add | See |
|---|---|
| Subscriber list (DM N people, not just a channel) | cookbook: event fan-out |
| Conditional output (different message for different stream categories) | guide/rules.md — [[conditions]] with Or |
| Per-platform retry on failure | guide/rules.md — retry policy on actions |
| Tracking which platforms succeeded vs failed | springtale-cli events --connector … |
| Tying this into a moderation bot | tutorial 01 |
- The fan-out rule has access to every platform's send capability. Sentinel logs each dispatched action; review the audit trail periodically.
- One compromised connector ≠ all compromised. Each connector's credentials are isolated in the vault; a Kick OAuth refresh failure doesn't expose your Bluesky app password.
- Rate limits apply per-platform. If you fan out to many subscribers, you'll hit Telegram's 30/sec or Discord's 50/sec bursts. The scheduler retries with exponential backoff — see guide/rules.md.
springtale-cli rule delete stream-live-fanout
springtale-cli connector remove connector-kick
springtale-cli connector remove connector-bluesky
springtale-cli connector remove connector-discord
springtale-cli connector remove connector-telegram