NextCRM

Background jobs

What runs in Inngest, on which schedule, what each job needs, and how to run Inngest in production.

NextCRM runs asynchronous and scheduled work with Inngest. The app serves its functions at /api/inngest. An Inngest server calls that endpoint to run them, and the app sends events to the server when something happens (a record is saved, a document is uploaded).

If Inngest is not running, the app still works, but nothing below happens: no embeddings, no email sync, no scheduled sends.

Scheduled jobs

JobSchedule (cron)What it doesNeeds
Email: Sync All Accounts*/15 * * * *Syncs every active user mailboxUsers' IMAP settings
CRM: Google Calendar sync (all connections)*/15 * * * *Syncs every active Google Calendar connectionGoogle Calendar OAuth client
Reports: Send Scheduled*/15 * * * *Sends report schedules that are dueRESEND_API_KEY, RESEND_FROM_EMAIL
CRM: 45-day kill rule0 6 * * *Closes deals in a qualified stage after the configured days of client silence and notifies the repFunnel settings; Resend for the notification
CRM: Recycle exhausted targets30 6 * * *Moves targets to a "Recycled" list after their sequence ended and sends admins a digestRESEND_API_KEY, RESEND_FROM_EMAIL
CRM: Renewal reminders0 7 * * 1Creates tasks for upcoming contract and product renewalsFunnel settings
ECB Sync Exchange Rates0 17 * * 1-5Fetches ECB reference ratesECB auto-update switched on under Currencies

Inngest evaluates cron expressions in UTC.

Event-driven jobs

AreaJobsTriggerNeeds
EmbeddingsEmbed Account, Contact, Lead, OpportunityA record is savedOPENAI_API_KEY (environment only)
EmbeddingsEmbed Backfill All CRM RecordsEvent crm/backfill.requested, sent manuallyOPENAI_API_KEY
EmailSync Account, Link to CRM, Embed EmailMailbox syncIMAP access; OPENAI_API_KEY for embeddings
EnrichmentEnrich Contact, Enrich Contacts BulkUser starts enrichmentOpenAI and Firecrawl keys
EnrichmentEnrich Target, Enrich Targets Bulk, Enrich Target ContactUser or MCP starts enrichmentAnthropic key, E2B_API_KEY
CampaignsSchedule Send, Send Now, Send Step, Process Follow-upUser sends or schedules a campaignRESEND_API_KEY, RESEND_FROM_EMAIL
Sales automationQualified follow-up cadence, Care touchpointsAn opportunity changes stageSales stages with automation triggers
CalendarProcess calendar event, Google Calendar sync (one connection), Calendar outbound syncCalendly webhook, sync, CRM changesCalendar setup
DocumentsEnrich Document, Generate Document ThumbnailA document is uploadedOPENAI_API_KEY for enrichment

Installed plugins can add their own scheduled and event-driven functions; they are registered with the rest.

Backfill embeddings

Records that existed before you set OPENAI_API_KEY have no embeddings, so they do not show up in semantic search or "Find similar". To embed them, send the event crm/backfill.requested from the Inngest dashboard. With the bundled server, uncomment the 8288 port mapping of the inngest service in docker-compose.yml (or use an SSH tunnel), open the dashboard and send the event. Close the port again afterwards.

Running Inngest

Both compose files run the self-hosted Inngest server in production mode (inngest start -u http://app:3000/api/inngest) and set on the app:

INNGEST_DEV=0
INNGEST_BASE_URL=http://inngest:8288
INNGEST_EVENT_KEY=<from env or generated>
INNGEST_SIGNING_KEY=<from env or generated>

Every request between the server and /api/inngest is signed with INNGEST_SIGNING_KEY; the app rejects unsigned ones, so the public endpoint cannot be used to trigger jobs. If you leave the two keys empty, the app generates them on the first start and stores them in the app_data volume, which the inngest service mounts read-only. If you set them yourself, the signing key must be a hex string (openssl rand -hex 32).

Queued runs, schedules and sleeps are kept in the inngest_data volume, so they survive restarts. The server checks the app for new or changed functions every 60 seconds.

INNGEST_DEV=1 (the SDK's dev mode, which skips signature checks) is meant only for local development with pnpm dev and the dev server from docker-compose.dev.yml. Never set it on an instance reachable from a network.

On this page