Artisan Command Reference¶
Yaya Engine ships a large custom Artisan surface split across five purpose
groups: Rada weather ingestion and load-testing, one-off Supabase backfills
tied to the Rada v2 cutover, Telegram webhook and broadcast management, MCP
Passport client provisioning, and locale/admin housekeeping. Most of the
backfill and Rada commands are Laravel ports of scripts that used to run
directly against Supabase before Rada v2 moved that data into yaya-engine's
own Postgres; the rest are the
scheduled jobs (routes/console.php) that keep alerts, notifications, and
message batches flowing without manual intervention. This page lists every
command's signature and what it actually does; for how to invoke the
Rada-facing ones against a live environment see
Run the cross-repo harness.
Rada weather and load-testing¶
rada-weather:ingest-woredas and rada-weather:update-thresholds are direct
Laravel ports of Python scripts that used to run against Supabase
(ingest_ethiopia_woredas.py / update_ethiopia_thresholds.py). Both read a
10-column CSV by position, not by header name — town_id, country,
country_name, state, town, latitude, longitude, yellow, orange, red — because
matching by position survives spreadsheet exports that rename headers
[@ingest-woredas] [@update-thresholds]. ingest-woredas upserts both towns
(keyed on country, state, town) and states (keyed on country, state),
deriving each state's thresholds as the max of its woredas' thresholds so
a state-level alert never fires when no woreda within it would
[@ingest-woredas]. update-thresholds only touches the yellow/orange/red
columns on existing towns — matching is case-insensitive on state/town so
an operator's spreadsheet casing doesn't need to match the database exactly —
and requires --force to skip a confirmation prompt outside of --dry-run
[@update-thresholds].
| Command | Purpose |
|---|---|
rada-weather:ingest-woredas {source} {--dry-run} {--country=ET} |
Ingest ADM1 (states) + ADM3 (towns) geography and 24h rain thresholds from CSV [@ingest-woredas] |
rada-weather:update-thresholds {source} {--dry-run} {--force} {--country=ET} |
Update thresholds only on existing towns from CSV; does not touch identity or lat/lon [@update-thresholds] |
rada:loadtest:composer-bulk-insert {--base-url=} {--token=} {--rows=21000} {--chunk=5000} {--alert-id=999999} {--country=Colombia} {--sla-seconds=1800} {--timeout=120} |
Drives the live /api/v1/composed-messages/bulk endpoint with composer-shaped payloads to verify the 21,000-rows-per-30-minute SLA the Python composer requires. Does not clean up the rows it inserts — the operator deletes them by the supplied --alert-id, or runs against a disposable environment [@composer-bulk-insert] |
rada:prune-forecast-store {--days=90} {--dry-run} |
Deletes superseded (non-latest) town_forecasts/town_observations rows older than the retention window; the latest row per group is always kept. Scheduled daily at 01:00 [@prune-forecast-store] [@console-schedule] |
rada:loadtest:composer-bulk-insert is deliberately different from the Pest
harness in tests/Performance/: it issues real HTTP calls, so it measures
the full network + serialization + Postgres stack rather than an in-process
benchmark [@composer-bulk-insert].
Supabase backfills (Rada v2 cutover)¶
These commands move data that used to live only in Supabase into yaya-engine's own tables, and are designed to be safely re-run: every one is idempotent, upserting on the same unique key the destination table's migration defines, so a repeated run converges instead of duplicating rows.
| Command | Purpose |
|---|---|
rada:backfill-from-supabase {--dry-run} {--table=} {--batch-size=500} {--page-size=5000} {--max-orphan-ratio=} {--resume-from-id=} |
Backfills user_nearest_town → user_nearest_towns and templates → meta_templates from Supabase PostgREST. Keyset-paginated per table — user_nearest_town's key is the uuid user_id column, not id, because that table has no id column; keying on the wrong column made PostgREST 42703 and silently copy zero rows (ye#382) [@backfill-from-supabase] |
rada:backfill-geography {--table=both\|towns\|states} {--dry-run} {--batch-size=500} {--page-size=1000} {--resume-from-id=} |
Backfills towns_24h_rain_thresholds + states_24h_rain_thresholds into local towns/states. Maps Supabase's full country name to ISO-2 via a small built-in lookup (Ethiopia→ET, Kenya→KE, Colombia→CO); unknown countries are skipped and logged rather than crashing the run [@backfill-geography] |
rada:backfill-user-country {--dry-run} {--chunk=500} |
Backfills blank users.country from the user's FSP country, reusing User::inheritCountryFromFsp() — the same rule the registration hook applies — so self-serve signups (Telegram, OTP) that never collected a country stop reporting zero in country-scoped dashboards. Dumps a before-state CSV to storage/app/backfill-user-country/ before writing, so a bad run is reversible [@backfill-user-country] |
supabase:sync-users {--dry-run} |
Syncs users without a supabase_user_id to the legacy Supabase database; no-ops if SUPABASE_SYNC_ENABLED=false [@sync-users-to-supabase] |
supabase:sync-missing-alert-subscriptions {--dry-run} |
Backfills alert subscriptions in Supabase for users who have a confirmed location but no active subscription there [@sync-missing-alert-subscriptions] |
Both rada:backfill-from-supabase and rada:backfill-geography guard on an
orphan-failure ratio: if more than 5% of processed rows can't be matched
to a yaya-engine record (a missing user, or an unmapped country) after more
than 100 rows are processed, the run aborts rather than silently dropping a
large fraction of data, and reports the last committed id so the operator can
resume with --resume-from-id [@backfill-from-supabase]
[@backfill-geography].
Telegram¶
| Command | Purpose |
|---|---|
telegram:set-webhook |
Registers the Telegram webhook URL with Telegram [@telegram-set-webhook] |
telegram:unset-webhook {--drop-pending} |
Removes the webhook (rollback or a switch to polling) [@telegram-unset-webhook] |
telegram:smoke {--base=} |
Smoke-tests the webhook endpoint: expects 404 with bad secrets, 200 with the configured ones [@telegram-smoke] |
telegram:broadcast-location-request {--dry-run} {--variant=generic\|flood} |
One-off backfill: dispatches a location-share prompt to existing Telegram bot users who never confirmed a location. --variant=flood sends the Ethiopia flood/rain Amharic copy instead of the generic copy, tracked on a separate "prompted once" column per variant so the two campaigns never collide [@telegram-broadcast-location-request] |
telegram:request-phone |
Sends a Telegram message with a Mini-App button that deep-links to /payout/phone [@telegram-request-phone] |
telegram:send-phone-reminders |
Dispatches one-time phone-number reminders to users registered 48h+ ago with a linked Telegram account but no phone. Scheduled hourly [@telegram-send-phone-reminders] [@console-schedule] |
telegram:send-alert-phone-followups |
30 minutes after an alert event, re-prompts Telegram recipients who still have no phone number; idempotent catch-up over the last 24h. Scheduled every 10 minutes [@telegram-send-alert-phone-followups] [@console-schedule] |
telegram:broadcast-location-request's variant selection is a command flag,
never derived from the user's locale — the flood copy is an Ethiopia
campaign, not an Amharic-locale rule [@telegram-broadcast-location-request].
MCP client provisioning¶
| Command | Purpose |
|---|---|
mcp:ensure-personal-access-client |
Idempotently ensures a Passport personal-access grant client exists for the admin_users provider — the client MCP token minting uses to issue tokens for InternalYayaServer callers. Safe to re-run: reports the existing client id instead of creating a duplicate [@ensure-personal-access-client] |
Locale and admin¶
| Command | Purpose |
|---|---|
users:align-fsp-locale {--dry-run} {--fsp=} |
Backfills user locale to match the FSP's default_locale override where one is configured, optionally scoped to one FSP by id or slug [@align-fsp-locale] |
admin:create |
Creates a new admin user for the Filament admin panel [@admin-create] |
admin:assign-role {--email=} {--role=} |
Assigns a shared RBAC access role (from AdminAccess::roleNames()) to an existing admin user, syncing permissions first [@admin-assign-role] |
users:phone-dedup-report |
Reports users with duplicate non-null phone_number values [@phone-dedup-report] |
Scheduled notification, alert, and maintenance jobs¶
The remaining commands are almost entirely driven by routes/console.php's
Schedule::command(...) entries rather than run ad hoc. Grouped by what they
maintain:
| Command | Schedule | Purpose |
|---|---|---|
notifications:task-reminders |
hourly | Push reminder 24h after action assignment with no activation [@notifications-task-reminders] [@console-schedule] |
notifications:task-window-closing |
hourly | Push ~6h before an action expires |
notifications:action-dropoff-reminders |
hourly | Push for users who started but didn't complete an action |
notifications:pre-alert-reengagement |
hourly | Push to onboarded users with no engagement in 24h |
notifications:season-kickoff |
manual | Broadcast: seasonal preparedness kickoff push |
notifications:feature-announcement |
manual | Broadcast: announce a new feature via push |
notifications:app-update |
manual, post-deploy | Broadcast: nudge users to update the PWA [@notifications-app-update] |
notifications:prune {--days=90} |
daily | Deletes read manager notifications past retention |
alerts:retarget |
hourly | Post-alert retargeting SMS to users who haven't opened the app [@alerts-retarget] |
alerts:retry-missed |
manual | Re-dispatches messages to alert recipients never contacted due to batch failures [@alerts-retry-missed] |
actions:expire |
hourly | Expires assigned user actions past their expiration window |
events:auto-close |
daily 00:30 | Closes active events whose end_date has passed |
marketing:process-flows |
every 15 min | Processes active marketing flows and sends eligible touchpoints |
messaging:poll-bonga-delivery |
every 5 min | Polls BongaSMS delivery status for pending SMS messages |
messaging:prune-delivery-logs |
daily | Prunes and redacts old message delivery logs |
slack:daily-digest |
daily 08:00 | Sends a daily operations digest to Slack |
exports:cleanup {--hours=1} |
hourly | Deletes stale export files from the private exports directory |
Development and one-off utilities¶
| Command | Purpose |
|---|---|
app:convert-action-workbook {path} |
Converts an action workbook XLSX into seeder JSON (action_knowledge_base.json + action_translations.json) [@convert-action-workbook] |
mixpanel:backfill-profiles {--dry-run} |
Backfills Mixpanel profile properties for existing users |
push:test |
Sends a test FCM push and dumps the payload/response for debugging |
test:retargeting-scenario {user_id} {--severity=red} |
Creates a test scenario for post-alert retargeting (an alert with a past event_date) |