Workflows
Durable work lives in lib/workflows/functions/ and is served together from
app/api/inngest/route.ts. Each side effect is its own step.run, so it retries independently and is
skipped on replay; step.sleep provides real delays without holding concurrency.
Registered functions
| Function | Trigger event | Retries | Concurrency | Purpose |
|---|---|---|---|---|
|
|
0 |
5 |
Window check → pre-validate → dial |
|
|
0 |
20 |
Double tap, contact update, AI analysis |
|
cron |
2 |
1 |
Sum phone-call |
|
|
0 |
10 |
AI analysis of a human rep’s call |
|
|
2 |
20 |
Send one campaign email |
|
|
2 |
5 |
Chunked recipient copy |
|
|
2 |
5 |
Chunked segment member copy |
|
|
2 |
1 per |
Fill staging from Outreach filters or BigQuery SQL, insert newcomers, soft-close this strategy’s dropouts when the scan is complete |
|
cron |
2 |
— |
List active re-engagement segments and enqueue one refresh event per Outreach or BigQuery strategy |
|
|
2 |
1 per |
Pick today’s list from open members (score desc, volume cap, stale skip), persist HubSpot list ids, add assigned contacts to those lists |
|
cron |
2 |
1 per |
Find-or-create today’s HubSpot daily segments for active sales reps, attach tenant-scoped list ids to pick events, then enqueue one daily-list event per active Outreach or BigQuery re-engagement strategy |
|
cron |
2 |
1 |
One shared HubSpot list, |
|
|
1 |
50 |
Run a contact’s whole sequence |
|
cron |
2 |
1 |
Pull up to 1000 HubSpot contacts missing from the Outreach BQ mirror and upsert them into |
|
cron |
2 |
1 |
Pull up to 1000 HubSpot CALL engagements from |
|
cron |
2 |
1 |
Incremental upsert from |
|
cron |
2 |
1 |
Delete |
|
cron |
2 |
1 |
Zero |
|
cron |
2 |
1 |
Zero |
|
cron |
2 |
1 |
Release numbers unused for 3+ days with fewer than 10 lifetime calls. |
|
|
0 |
1 |
Buy and register a number (body currently disabled) |
|
— |
— |
— |
Scaffolding / test function |
|
— |
— |
— |
Scaffolding / test function |
A function that is not in the functions array of app/api/inngest/route.ts will never run.
batchCallAnalysisWorkflow (lib/workflows/batch-call-analysis-workflow.ts) is not an Inngest
function — it uses the 'use workflow' / 'use step' directive runtime. See
Batch call analysis.
Client
lib/plugins/inngest-client.ts creates the client with id dialer-v2-app and installs
@inngest/middleware-sentry, so a failing step is reported with its workflow context. Sentry is
initialised in the same module.
Guard mechanisms
The functions use four distinct Inngest guards. Knowing which one applies is often the answer to "why did this not run twice".
| Mechanism | Where | Effect |
|---|---|---|
|
|
A discrete business action must not be repeated automatically. Retrying a dial would call the prospect twice |
|
|
Excess events are dropped, not queued. This is the hard ceiling behind all the softer campaign limits |
|
|
Only one purchase in flight per state and area code per workspace |
|
|
Tears the run down immediately, including an in-progress |
Retry semantics
retries |
Meaning here |
|---|---|
|
The action has an external side effect that must not repeat. Failures are returned as data ( |
|
|
|
|
Diagram
flowchart TD
Cron1["pg_cron"] --> B1["POST /api/campaigns/{id}/broadcast"]
B1 -->|"call-target/call.requested"| CT["callTarget"]
CT -->|Retell API| Retell["Retell AI"]
Retell -->|"webhook call_analyzed"| WH["/api/webhooks/event-logger"]
WH -->|"call-target/call.ended"| CEA["callEndedAnalysis"]
CEA -->|"double tap: sleep 1m"| CT
CEA --> Analysis["CallAnalysisService.runAndSave"]
Cron2["pg_cron"] --> B2["POST /api/email-campaigns/{id}/broadcast"]
B2 -->|"email-campaign/send.requested"| ES["emailSend"]
ES -->|Resend API| Resend["Resend"]
Resend -->|webhooks| WH2["/api/webhooks/resend"]
NooksWH["/api/webhooks/nooks"] -->|"nooks/call.logged"| NCA["nooksCallAnalysis"]
Enroll["POST /api/contact-sequences/{id}/start"] -->|"sequence/execute"| SE["sequenceExecute"]
Stop["POST /api/contact-sequences/{id}/stop"] -->|"sequence/cancel"| SE
Dup["POST /api/email-campaigns/{id}/duplicate"] -->|"email-campaign/duplicate.requested"| ECD["emailCampaignDuplicate"]
Seg["POST .../targets/from-segment"] -->|"email-campaign/import-segment.requested"| ECS["emailCampaignImportSegment"]
Refresh["POST /api/segments/{id}/refresh"] -->|"segment/refresh.requested"| SR["segmentRefresh"]
CronSeg["segmentRefreshCron 06:00/18:00 ET"] -->|"segment/refresh.requested"| SR
Daily["POST /api/segments/{id}/daily-list"] -->|"segment/daily-list.requested"| DL["reengagementDailyList"]
DailyWs["POST /api/reengagement/daily-list"] -->|"segment/daily-list-cron.requested"| CronDaily
CronDaily["reengagementDailyListCron"] -->|"segment/daily-list.requested"| DL
CronNever["reengagementNeverCalledDailyList 07:30 ET"] --> HsNever["Outreach-Never-Called list"]
CronHs["hubspotContactImport 06:00/10:00/14:00/18:00/22:00 ET"] --> BQ["hubspot4.v_contacts_latest"]
CronHs --> Contacts["Contact + TargetPhoneNumbers + TargetEmails"]
CronHsCall["hubspotCallImport 08:00/20:00 ET"] --> BQCalls["hubspot4.v_calls_latest"]
CronHsCall --> Calls["Call"]
CronSup["payabilitySupplierImport 06:00/10:00/14:00/18:00/22:00 ET"] --> BQSup["PayabilitySheets.v_supplier_summary_extended"]
CronSup --> Suppliers["Supplier"]
Conventions
-
Name steps descriptively and stably — step ids are the replay key.
sequenceExecuteembeds the step index, retry count, channel and a slugified name so the dashboard is readable and ids survive replays. -
Keep each
step.runto one side effect. A step that does two things cannot retry half of them. -
Import heavy dependencies inside the step (
await import(…)) where the function is otherwise cheap to load. -
Return structured data (
{ skipped, reason, skipReason }) rather than throwing for expected outcomes. -
Decrement the Redis in-flight counter on every exit path, not just the happy one.