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), 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_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.
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, 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).