Automation engine (Phase 8)
"WHEN trigger [ONLY IF conditions] DO actions" recipes, executed by the queue worker with a full per-action log in automation_runs.
API
$app->make('automation')->trigger(int $tenantId, string $event, array $payload, int $depth = 0): array // queued automation ids
$app->make('automation')->execute(int $tenantId, int $automationId, array $payload, int $depth = 0, bool $test = false): array // run row
$app->make('automation')->cronTick(?int $ts = null): int
Call trigger() from any module after the fact happened (guard with $app->has('automation')). Payload keys are free-form; the builder documents them per trigger (App\Automation\Triggers::ALL[...]['sample']).
Triggers
stream.started, stream.ended (fired by ProfileService::setLive), order.paid, subscription.canceled (Store webhooks), order.shipped (merch tracking number appears, docs/merch.md), member.joined (Communities), server.player_count_above (Servers heartbeat; trigger_config.threshold, hysteresis: fires once when crossing above, re-arms when the count drops below), post.published (Publisher), schedule.cron (trigger_config.cron, 5-field UTC expression evaluated every minute by the scheduler task automation.cron).
Conditions
JSON list of {field, op, value}; field is a dot path into {payload, trigger, event} (product is shorthand for payload.product). Ops: eq neq gt gte lt lte contains in.
Actions
update_profile_live, post_social (public), post_reddit_live (public; see below), post_discord (public; bot via $app->make('discord')->announce else the encrypted workspace webhook), notify_followers, create_entitlement, grant_discord_role, update_fivem_permissions, send_email, run_ai, webhook (HMAC X-Signature: sha256=…, private hosts blocked), add_to_crm_export (storage/exports/{tenant}.csv). Config strings accept {{payload.}}, {{profile.}}, {{tenant.name}}. Each action class declares LABEL, FIELDS (drives the builder UI) and PUBLIC.
Automations containing a public action can only be enabled by owners/admins. Test runs (POST /dashboard/automations/{id}/test) execute with test = true: actions report what they would do and never post externally.
Showing a live stream on Reddit (post_reddit_live)
Reddit renders a link post as a card with the page's og:image, and for Twitch and YouTube URLs as an inline
player, so a link post in a subreddit is how a live feed "shows on Reddit". The action (App\Automation\Actions\ShareLiveOnReddit)
takes subreddit (r/FiveM, FiveM or u_name for the creator's own profile), title (placeholders, trailing :
trimmed when {{payload.title}} is empty, capped at 300) and an optional url. The link is the first of: the action's
url, the trigger's payload.url (set by the Twitch/Kick/YouTube poll and EventSub), the profile's saved live URL,
the connected Twitch or Kick channel the Krunc relay is pushing to (payload.destinations), the creator's /@handle
page. It writes r/sub\ntitle\nurl as a post + one post_targets row per active Reddit account and hands it to the
publish queue, so the run log says "queued" and the Publisher page carries the Reddit error or the post URL. The
Reddit provider submits with resubmit=true so the same channel URL can be posted again at the next go-live.
Krunc-relay go-lives (IngestService::startSession) fire stream.started with handle, display_name, title
(the Stream title card, else the profile's last live title) and url (only when the profile has a saved live URL)
alongside source, session_id and destinations, so the Discord/X/Reddit actions have something to say.
Loop protection
- Max trigger depth 3 (
Engine::MAX_DEPTH); actions that emit further triggers passdepth + 1. - Identical payload for the same automation deduped within the same minute (
jobs.dedupe_key). - Max 20 runs per automation per 10 minutes → run logged as
skipped. update_profile_liveis a no-op when the state already matches.
Recipes
Recipes::install($app, $tenantId, key, enabled=false) — when_i_go_live, share_live_on_reddit (subreddit left blank on purpose: the run fails with "No subreddit set" until the owner fills it in), when_i_end_stream, when_someone_buys_vip. Installed disabled so the owner reviews the actions first.
Worker
php bin/worker (daemon) or php bin/worker --once from cron every minute. Queue automation; QUEUE_DRIVER=sync runs actions inline (default on this host).