Management commands
Operator-facing CLI commands for housekeeping, backfills, and
manually triggering scheduled jobs. Run via manage.py (or
docker exec ... manage.py if your platform runs in a container).
Most operators interact with these through their orchestrator (Cloud Run jobs, cron, Kubernetes CronJob) rather than running them by hand β but it's worth knowing what's available for debugging + one-off operations.
Quality + agreementβ
run_auto_suspend_rulesβ
Manually trigger an evaluation pass of every active auto-suspend rule across all orgs. Normally runs as a 24-hour scheduled job; run it directly if you want to validate rule behaviour or kick a delayed run.
python manage.py run_auto_suspend_rules
Flags:
--org <N>β limit to a specific org--dry-runβ report which members would be warned / suspended without writing or notifying. Useful when rolling out a new rule--no-rescheduleβ skip the "enqueue next run" step (testing)
backfill_agreementβ
Recompute Task.precomputed_agreement for tasks that don't have
a score yet (or whose score is stale after a metric change).
python manage.py backfill_agreement [--project <N>] [--dry-run]
Omit --project to scan every project.
Useful after:
- Changing a project's agreement metric
- Importing historical annotations that didn't trigger live computation
- A bug-fix to an agreement-metrics function (rescore everything)
detect_label_driftβ
Scan projects for per-annotator label-distribution drift β an annotator whose recent label mix has diverged from their own long-run baseline β useful for catching cases where someone's interpretation of "POSITIVE" drifted over months without anyone noticing.
python manage.py detect_label_drift [--project <N>] [--dry-run]
Omit --project to scan every project. Flagged annotators become
review items in the project (no file output); the command prints
a per-project summary and a final
projects_scanned=β¦ annotators_flagged=β¦ line to stdout. --dry-run
scans and reports without creating review items.
Housekeepingβ
expire_pending_invitesβ
Hard-delete invite records that are older than the org's invite- expiry policy + grace window. Usually runs as a 24-hour job; run by hand if your invite table is bloated.
python manage.py expire_pending_invites [--days 14] [--dry-run]
send_inactivity_remindersβ
Send email reminders to org members who haven't logged in for N days. Usually runs as a 24-hour job; run by hand for testing.
python manage.py send_inactivity_reminders [--days 14] [--limit 500] [--dry-run]
Dry-run shows who would receive a reminder without sending.
send_notification_digestsβ
Send periodic notification digest emails to users who opted in from Profile β Preferences (off / daily / weekly, default off). Each digest summarises what happened since the user's last one β review outcomes on their annotations, contribution-goal progress, newly required consents, and new public projects on Discover β and is only sent when there's something to report.
python manage.py send_notification_digests [--frequency daily|weekly] [--dry-run]
Schedule it once a day (cron, Cloud Run job, Kubernetes CronJob); each user's own frequency preference decides whether they are due on a given run. Idempotent β re-running the same day sends nothing, and content never repeats across digests.
The command also flushes stale review verdicts before building
digests: any annotation reviewed more than an hour ago whose
annotator was never told (the live rollup only fires when a
reviewer's queue drains) gets the standard "your work has been
reviewed" notification, so a rejection can never sit unannounced
in a half-finished queue
(last_digest_sent_at is stamped per user after each send). The
"digest" notification kind respects the platform-wide email
kill-switch in Notification Settings.
Flags:
--frequency daily|weeklyβ restrict a run to one cadence (useful if you want separate schedules); default processes both--dry-runβ report who would get a digest and which sections, without sending or stamping
cleanup_exportsβ
Delete export snapshots older than each org's
Delete export snapshots after (days) policy
(retention.export_files_max_age_days; 0 / unset = keep forever).
Removes the Export row, its snapshot file, and any converted-format
files. The policy applies whether or not it is marked Enforced.
python manage.py cleanup_exports [--dry-run]
Schedule daily alongside the other maintenance commands. --dry-run
lists what would be deleted without deleting.
measure_waiting_filesβ
Measures the media files of projects where the Data Manager's Measure
files action could not finish in one go (it notes the project and
leaves the rest to this command). A project is done, and dropped from
the list, once nothing in it is left to measure. Part of maintenance.
python manage.py measure_waiting_files [--minutes 20]
purge_expired_project_dataβ
Retention sweep for projects whose effective retention window is > 0
days (per-project data_retention_days, else the org's
retention.project_data_max_age_days). For each such project it
computes the deletion date from the oldest task's created_at
(project created_at if it has no tasks), sends escalating owner
warnings (in-app + email) at 30 / 7 / 1 days before β each once β
and, once the deadline passes, deletes tasks, annotations, drafts,
predictions, uploaded media files, and export snapshots while
keeping the Project row as an empty shell. Bookkeeping lives in
Project.retention_state (warnings_sent, purged_at); an
already-purged project with no newer data is skipped.
python manage.py purge_expired_project_data [--dry-run]
Schedule daily. --dry-run reports what would be warned / purged
without changing anything. See Org policies.
cleanup_orphan_hf_tasksβ
Delete tasks that were imported from Hugging Face Hub
(Task.meta.hf_dataset_id set) but whose project's HF source binding
is gone or now points at a different dataset β leftovers from a
cleared or swapped binding that can no longer push annotations back.
Annotations, drafts, and predictions cascade with the tasks.
python manage.py cleanup_orphan_hf_tasks [--project <N>] [--dry-run]
One-off; run after noticing stranded HF tasks in the Data Manager.
Omit --project to scan every project. See
Hugging Face Hub.
prune_legacy_tokensβ
Blacklist accumulated legacy refresh tokens (minted before
desktop / mobile tokens carried a device claim) so a user's
Personal Access Token list shows at most one. Per user it keeps every
token with a device claim plus the single newest unclaimed token;
every other unclaimed token is blacklisted.
python manage.py prune_legacy_tokens # dry run, all users
python manage.py prune_legacy_tokens --apply
python manage.py prune_legacy_tokens --user someone@example.com --apply
Dry-run by default; --apply writes. One-time cleanup after upgrading
past the device-claim change.
annotations_fill_updated_byβ
One-time migration helper that backfills Annotation.updated_by
for historical annotations imported before that field existed.
Idempotent. Useful when upgrading from a very old version.
python manage.py annotations_fill_updated_by
calculate_stats / calculate_stats_all_orgsβ
Recompute task / annotation aggregate stats stored on Project +
Organization records. Normally maintained live by signals; the
command is for fixing drift after a manual database edit or
migration.
python manage.py calculate_stats <organization_id>
python manage.py calculate_stats_all_orgs [--from-scratch] [--redis]
Backfills + repairsβ
backfill_clip_durationsβ
Fill Annotation.duration_seconds for speech clips recorded before
the audio-QC dispatcher started storing durations at ingest (the
field feeds contribution hours + goals). Only touches annotations
whose result carries an audio URL and that have no duration yet, so
it is safe to re-run and resumable. Durations are read header-only via
soundfile; without it, WAV files fall back to a RIFF header parse
and other formats are left null for a later run.
python manage.py backfill_clip_durations [--project <N>] [--batch-size 200] \
[--limit <N>] [--start-id <id>] [--dry-run]
Flags: --project (one project id), --batch-size (annotations per
batch, default 200), --limit (stop after N candidates), --start-id
(resume after this annotation id), --dry-run (report, write
nothing). One-off; run once after upgrading, or again if a run was
interrupted. See Goals and progress.
fix_record_upload_urlsβ
Repair AudioRecord / VideoRecord results (and drafts) saved with the
old id-shaped URL /data/upload/<id>, which the path-based media
route cannot serve β so replay and export failed. Rewrites each to
the real /data/upload/<project>/<uuid>-β¦.webm path by joining the
id against FileUpload.
python manage.py fix_record_upload_urls # dry-run report
python manage.py fix_record_upload_urls --apply # write changes
python manage.py fix_record_upload_urls --project 12 --apply
Dry-run by default; --apply writes; --project scopes to one
project. Run once on deployments that hold recordings made before the
fix. See AudioRecord.
Deployment + infrastructureβ
locked_migrateβ
Wrapper around migrate that takes an advisory database lock
before running. Use on multi-instance deployments (Cloud Run with
min-instances > 0, Kubernetes Deployments with replicas > 1)
where two pods could otherwise race to apply migrations.
python manage.py locked_migrate
Behaves identically to migrate if no other instance is running;
otherwise waits for the lock holder to finish. Mandatory on Cloud
Run with --max-instances > 1.
show_async_migrationsβ
List in-flight async migrations + their progress. Some migrations
run as background workers (tracked in the AsyncMigrationStatus
table); this command surfaces them.
python manage.py show_async_migrations
Useful when migrate finishes instantly but the platform's still
catching up on a big backfill behind the scenes.
send_test_emailβ
Send a test email to verify SMTP wiring without going through the signup / forgot-password flow.
python manage.py send_test_email you@example.com
Reads the standard EMAIL_HOST / EMAIL_HOST_USER /
EMAIL_HOST_PASSWORD env vars (see Email delivery) and
sends a fixed-template message. If you don't receive it, your SMTP
isn't wired correctly β the first diagnostic when "the platform
doesn't send mail."
Org lifecycleβ
destroy_organizationβ
Hard-purge an org + everything under it (projects, tasks, annotations, member rows). Bypasses the 30-day retention window.
python manage.py destroy_organization <organization_id>
Interactive: prints the org id + title and asks
Are you sure? yes/no: β only a literal yes proceeds (so it can't
be run unattended by accident). Use for testing deployments where
seed orgs need cleaning up, or for compliance-mandated
immediate-purge requests. It does not write an audit event of its
own.
seed_sample_projectsβ
Bootstrap a fresh platform with ~34 demo projects spanning text / NLP, image, document, video, audio, and time-series interfaces.
python manage.py seed_sample_projects [--org "<title>"] [--owner <email>] \
[--tasks-per-project 12] [--reset]
--org is the organisation title (default AfriAnnotate Testing, created if missing); --owner must already exist (run
seed_testers first). --reset deletes same-titled demo projects
before recreating. Uses synthetic data from a small bundled corpus;
nothing leaves the deployment. Useful for demo platforms + giving
operators a starting point to explore the UI.
seed_testersβ
Pre-create a fixed set of role-spread tester accounts (owner, admin, managers, reviewers, annotators) in a target org.
# Password mode (default): shared known password, printed at the end
python manage.py seed_testers [--org "<title>"] [--password <pw>] [--reset]
# Invite mode: inactive accounts, each emailed a set-password link
python manage.py seed_testers --send-invites --emails a@x.org,b@x.org \
[--base-url https://label.example.org]
Flags: --org (title, default AfriAnnotate Testing), --password,
--reset (re-stamp passwords / resend invites), --send-invites,
--emails (real addresses replace the default @afri.test ones in
role order), --base-url, --quiet. Useful for demo deployments,
integration tests, or practising the onboarding flow.
seed_agreement_demoβ
Seed one demo project with simulated multi-annotator annotations at a
known agreement level, for exercising the Quality settings + Metrics
tab. Creates Demo Β· Agreement Sandbox (<method>) in the target org;
idempotent on that title.
python manage.py seed_agreement_demo [--org "<title>"] [--owner <email>] \
[--tasks 40] [--annotators 4] [--agreement 0.75] [--method cohens_kappa] \
[--metric <key>] [--seed 42] [--reset]
--org (title, default AfriAnnotate Testing) and --owner (default
owner@afri.test) must already exist β run seed_testers first.
--agreement is the probability (0..1) each simulated annotator copies
the gold answer; --method sets the project's agreement_method;
--metric optionally pins an agreement_metric key (blank = auto per
control); --seed makes the run reproducible; --reset deletes the
existing demo project first.
seed_translation_demoβ
Seed a small machine-translation demo project
(Demo Β· Machine Translation (ENβYO): Text source + TextArea target),
a handful of tasks that deliberately overlap the seeded Translation
Memory (so fuzzy suggestions fire), some TM segments, and a small
glossary. Idempotent on the title.
python manage.py seed_translation_demo [--org "<title>"] [--owner <email>] [--reset]
Org title and owner email must already exist (the defaults are a
developer test org / account β pass your own). --reset deletes and
rebuilds.
seed_tm_test_corpusβ
Seed larger pre-translated corpora for smoke-testing TM accuracy
(fuzzy matching, glossary lookup, near-duplicate resolution). One
project per pair β TM Test Β· English β Hausa / Yoruba / Swahili β
each task pre-filled with an initial annotation (post-edit workflow),
the same pairs loaded into the TM, plus a glossary and intentional
near-duplicates.
python manage.py seed_tm_test_corpus [--org "<title>"] [--owner <email>] \
[--pair en-ha|en-yo|en-sw] [--count <N>] [--reset]
--pair empty = all three; --count caps tasks per project (0 =
full curated corpus); --reset rebuilds. Org / owner defaults are a
developer test org / account β pass your own.
Sync (desktop)β
afri_sync_pullβ
Manually trigger a pull from the cloud into the local SQLite mirror on a desktop install. Only useful on the desktop runtime; no-op on the cloud Django.
python manage.py afri_sync_pull [--cloud-url <url>] [--token <jwt>] [--full] [--note "<text>"]
--full ignores the delta anchor and re-pulls every task row.
Useful when a sync is stuck and you want to force-restart the pull loop. The same primitive runs as part of the desktop's scheduled sync.
afri_sync_pushβ
Symmetric to afri_sync_pull: push dirty local annotations + drafts
from a desktop install up to the cloud. Same run_push the desktop's
/api/afri/sync/push/ view calls in-process; the CLI form is for
testing, cron, or draining an outbox when the desktop web bundle is
broken.
python manage.py afri_sync_push [--cloud-url <url>] [--token <token>] \
[--note "<text>"] [--project-pk <N>]
--cloud-url / --token override β and persist to β the stored
CloudConfig; --note is written to SyncRun.note; --project-pk
narrows the push to one local project pk (not the cloud id),
default every dirty row.
Background jobs (RQ)β
Beyond explicit management commands, several jobs run through django-rq when Redis is reachable:
auto_suspend_rules_dailyβ the production auto-suspend runner; self-reschedules every 24 h from Django boot- Agreement precomputation β fires on annotation submit;
computes per-task agreement and updates
Task.precomputed_agreement - Webhook delivery β fires on subscribed events
- Export snapshot β generates the requested format
- Email send β notification emails are queued on the
lowqueue
No Redis? Nothing is lost. The notification dispatcher pings Redis (1 s timeout) before enqueueing; if it's unreachable, or the enqueue fails, the email is sent inline, synchronously in the request. Exports and webhooks use the same start-async-or-sync helper. A Cloud Run / desktop deployment without Redis therefore works β at the cost of SMTP latency on the request path.
Monitor via RQ Dashboard (/django-rq/, staff-only).
How operators should use this pageβ
Three patterns:
- Wire into orchestration β set up Cloud Run jobs / Kubernetes
CronJobs that run
run_auto_suspend_rules,expire_pending_invites,send_inactivity_reminders,send_notification_digests,cleanup_exports,purge_expired_project_data,measure_waiting_filesandclearsessionson a daily schedule (themaintenancecommand runs them all). The reference cloud uses Cloud Run jobs with Cloud Scheduler triggers;deploy/gcp/setup-maintenance.shis the template. - One-off backfills + repairs β when you import historical data
or change a metric, run
backfill_agreement; after upgrading, runbackfill_clip_durations,fix_record_upload_urls --apply, andprune_legacy_tokens --applyonce each as needed. - Debugging β when "the auto-suspend rules don't seem to be
firing," run
run_auto_suspend_rules --dry-runto see what the system would have done.
What's NOT a management commandβ
- Project / annotation creation β these are user actions; use the API
- Bulk delete / archive β use the Data Manager UI or API; the CLI doesn't reach into user workspaces
- Auth + session ops β use the user-facing flows; CLI doesn't have password-reset etc.
The CLI is for operator-level housekeeping, not application logic.
What's nextβ
- Auto-suspend β β the rule
system
run_auto_suspend_rulesevaluates - Agreement metrics β β what
backfill_agreementrecomputes - Offline sync β β what
afri_sync_pull/afri_sync_pushtrigger - Org policies β β the retention
settings
cleanup_exports+purge_expired_project_dataenforce