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 pass depth + 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_live is 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).