.agents/rules/jobs.md
src/BuildingBlocks/Jobs/. Read before enqueuing or scheduling work.
IJobServiceInject IJobService (Jobs/Services/IJobService.cs) and use it; don't call Hangfire's BackgroundJob directly in feature code.
jobService.Enqueue(() => mailService.SendAsync(req, CancellationToken.None)); // default queue
jobService.Enqueue("email", () => mailService.SendAsync(req, CancellationToken.None));
jobService.Schedule(() => DoLater(), TimeSpan.FromMinutes(5));
Queues: default, email (5 workers, 30s poll). Storage auto-selected from DatabaseOptions.Provider (Postgres/MSSQL).
IRecurringJobManagerIJobService has no recurring API. Register recurring jobs in the module's MapEndpoints with IRecurringJobManager.AddOrUpdate<T>(...), always TimeZoneInfo.Utc:
recurringJobs.AddOrUpdate<PurgeOrphanedFilesJob>("files:purge-orphaned",
j => j.RunAsync(CancellationToken.None), Cron.Hourly(), new() { TimeZone = TimeZoneInfo.Utc });
Examples in the tree: PurgeOrphanedFiles/PurgeDeletedFiles (Files), MonthlyInvoiceJob (Billing), AuditRetentionJob (Auditing), WebhookDispatchJob (Webhooks).
/jobs (default), behind HangfireOptions.UserName/Password basic auth — both [Required], password [MinLength(12)], so startup fails in non-dev if unset.
IMultiTenantContextSetter) before touching a tenant-filtered DbContext.NoOpJobService whose methods throw — surfaces any accidental enqueue during migration. Don't enqueue from migration/seed paths.FshJobActivator); inject what you need.