---
title: "Workflow Automation"
description: "Workflow automation engines orchestrate complex partner lifecycle operations, reward calculations, and asynchronous event pipelines within affiliate programs. By combining trigger architectures, co..."
last_updated: "2026-10-05T05:07:35.182658+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/automation-and-communications/workflow-automation"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [apps/web/app/ee/api/workflows/create-partner-commission/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts)
- [apps/web/app/ee/api/cron/commissions/referrals/create/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/commissions/referrals/create/route.ts)
- [apps/web/lib/partner-referrals/create-referral-commission.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partner-referrals/create-referral-commission.ts)
- [apps/web/scripts/programs/backfill-reuse-commission.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/programs/backfill-reuse-commission.ts)
- [apps/web/app/ee/api/workflows/partner-approved/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/partner-approved/route.ts)
- [apps/web/app/ee/api/cron/bounties/upsert-draft-submissions/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/bounties/upsert-draft-submissions/route.ts)
- [apps/web/lib/api/customers/reattribute-customer.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/customers/reattribute-customer.ts)
- [apps/web/prisma/schema/workflow.prisma](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/workflow.prisma)
- [apps/web/lib/api/workflows/execute-workflows.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workflows/execute-workflows.ts)
- [apps/web/app/ee/api/workflows/reattribute-customer/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/reattribute-customer/route.ts)
- [apps/web/lib/jobs/send-workflows.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/send-workflows.ts)
- [apps/web/lib/openapi/commissions/create-commission.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/commissions/create-commission.ts)
- [apps/web/app/ee/api/cron/commissions/referrals/backfill/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/commissions/referrals/backfill/route.ts)
- [apps/web/scripts/programs/5-import-customer-sales.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/programs/5-import-customer-sales.ts)
- [apps/web/lib/api/commissions/create-manual-commissions.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/create-manual-commissions.ts)
- [apps/web/lib/partner-referrals/create-network-referral-commission.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partner-referrals/create-network-referral-commission.ts)
- [apps/web/lib/api/workflows/types.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workflows/types.ts)
- [apps/web/scripts/customers/upheal/sync-stripe-invoices.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/customers/upheal/sync-stripe-invoices.ts)
- [apps/web/lib/partners/queue-partner-commission-creation.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/queue-partner-commission-creation.ts)
- [apps/web/lib/api/conversions/track-lead.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/conversions/track-lead.ts)
- [apps/web/app/ee/api/workflows/detach-discount/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/detach-discount/route.ts)
- [apps/web/lib/jobs/registry.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/registry.ts)
- [apps/web/scripts/customers/beehiiv/fix-case-a-complex.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/customers/beehiiv/fix-case-a-complex.ts)
- [apps/web/lib/zod/schemas/workflows.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/workflows.ts)
- [apps/web/lib/api/workflows/attribute-definitions.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workflows/attribute-definitions.ts)
- [apps/web/lib/postback/constants.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/postback/constants.ts)
- [apps/web/lib/zod/schemas/rewards.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/rewards.ts)
- [apps/web/lib/api/workflows/attribute-validators.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workflows/attribute-validators.ts)
- [apps/web/lib/api/workflows/check-workflow-conditions.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workflows/check-workflow-conditions.ts)
</details>

## Overview

Workflow automation engines orchestrate complex partner lifecycle operations, reward calculations, and asynchronous event pipelines within affiliate programs. By combining trigger architectures, condition evaluation models, and asynchronous dispatch pipelines, the system automates multi-step processes such as partner application approvals, commission generation, customer reattributions, and discount management. This infrastructure ensures reliable background execution, handles fraud checks, reconciles balances, and processes historical backfills without blocking core API requests or degrading performance.

Sources: [apps/web/app/ee/api/workflows/create-partner-commission/route.ts:164-180](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L164-L180), [apps/web/lib/api/workflows/execute-workflows.ts:1-43](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workflows/execute-workflows.ts#L1-L43), [apps/web/lib/jobs/send-workflows.ts:1-80](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/send-workflows.ts#L1-L80)

## Trigger Architecture and Action Model

### Overview

Workflow triggers and conditional actions are structured around strongly-typed schemas, database models, and validation routines. A workflow execution context is built upon event types such as `partnerEnrolled`, `leadRecorded`, `saleRecorded`, and `commissionRecorded`, pairing partner metrics, program enrollments, and workspace identities with defined trigger parameters and action payloads.

Sources: [apps/web/lib/api/workflows/types.ts:28-50](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workflows/types.ts#L28-L50)

### Trigger and Action Models

The database schema defines the core `Workflow` model alongside the `WorkflowTrigger` enum. The enum includes active triggers like `partnerEnrolled` and `partnerMetricsUpdated`, as well as legacy entries such as `clickRecorded`, `commissionEarned`, `leadRecorded`, and `saleRecorded` retained for backward compatibility. Each workflow links to a program, stores trigger conditions and actions as JSON fields, and supports indexing on `[programId, trigger]`.

| Model / Enum | Field Name | Type | Description |
| :--- | :--- | :--- | :--- |
| `WorkflowTrigger` | `partnerEnrolled` | Enum value | Triggered when a partner is enrolled (scheduled) |
| `WorkflowTrigger` | `partnerMetricsUpdated` | Enum value | Triggered when partner metrics are updated |
| `WorkflowTrigger` | `clickRecorded` | Enum value | Legacy trigger for recorded clicks |
| `WorkflowTrigger` | `commissionEarned` | Enum value | Legacy trigger for earned commissions |
| `Workflow` | `id` | String | Primary identifier |
| `Workflow` | `programId` | String | Foreign key to Program table |
| `Workflow` | `trigger` | WorkflowTrigger? | Optional trigger classification |
| `Workflow` | `triggerConditions` | Json | Serialized condition rules |
| `Workflow` | `actions` | Json | Serialized action payloads |

Sources: [apps/web/prisma/schema/workflow.prisma:1-29](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/workflow.prisma#L1-L29)

### Action Types and Validation Schemas

Workflows support discriminated union action types validated through Zod schemas. Actions can award bounties, send campaigns, or move partners between groups based on defined parameters.

```typescript
export const workflowActionSchema = z.discriminatedUnion("type", [
  z.object({
    type: z.literal(WORKFLOW_ACTION_TYPES.AwardBounty),
    data: z.object({
      bountyId: z.string(),
    }),
  }),
  z.object({
    type: z.literal(WORKFLOW_ACTION_TYPES.SendCampaign),
    data: z.object({
      campaignId: z.string(),
    }),
  }),
  z.object({
    type: z.literal(WORKFLOW_ACTION_TYPES.MoveGroup),
    data: z.object({
      groupId: z.string(),
    }),
  }),
]);
```

Sources: [apps/web/lib/zod/schemas/workflows.ts:68-89](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/workflows.ts#L68-L89)

### Condition Evaluation Call-Chain

Condition evaluation follows a strict validation pipeline when checking workflow rules against input parameters. The execution walk proceeds through checking conditions, verifying attributes, matching operators, and running validators:

`checkWorkflowConditions()` → retrieves attributes via `WORKFLOW_TYPE_ATTRIBUTES` → validates condition attribute presence → resolves `WORKFLOW_OPERATORS` definition → validates operator compatibility against attribute definition → runs `operatorDefinition.validate()` on condition values.

```typescript
export function checkWorkflowConditions({
  conditions,
  workflowType,
}: {
  conditions?: WorkflowCondition[] | null;
  workflowType: WorkflowType;
}): {
  valid: boolean;
  errors: string[];
} {
  if (!conditions || conditions.length === 0) {
    return { valid: true, errors: [] };
  }
  const attributes = WORKFLOW_TYPE_ATTRIBUTES[workflowType];
  const errors: string[] = [];
  for (let i = 0; i < conditions.length; i++) {
    const condition = conditions[i];
    if (!condition?.attribute) {
      errors.push(`Condition ${i + 1}: Please select an activity.`);
      continue;
    }
    const attributeDefinition = attributes[condition.attribute as keyof typeof attributes];
    if (!attributeDefinition) {
      errors.push(`Condition ${i + 1}: Invalid activity.`);
      continue;
    }
    const operatorDefinition = WORKFLOW_OPERATORS[condition.operator as keyof typeof WORKFLOW_OPERATORS];
    if (!operatorDefinition) {
      errors.push(`Condition ${i + 1}: Invalid operator.`);
      continue;
    }
    if (!(attributeDefinition.operators as readonly string[]).includes(condition.operator)) {
      errors.push(`Operator "${condition.operator}" is not valid for the activity "${condition.attribute}".`);
      continue;
    }
    if (attributeDefinition.inputType === "none") {
      continue;
    }
    if (condition.value == null) {
      errors.push(`Condition ${i + 1}: Please enter a value.`);
      continue;
    }
    try {
      operatorDefinition.validate(condition.value as any);
    } catch (error) {
      errors.push(`Condition ${i + 1}: ${error instanceof Error ? error.message : "Invalid value."}`);
    }
  }
  return { valid: errors.length === 0, errors };
}
```

Sources: [apps/web/lib/api/workflows/check-workflow-conditions.ts:8-92](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workflows/check-workflow-conditions.ts#L8-L92)

> [!WARNING]
> Attributes with an input type of `"none"`, such as `partnerJoined`, intentionally skip value validation checks and exit early during condition evaluation.

Sources: [apps/web/lib/api/workflows/check-workflow-conditions.ts:67-70](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workflows/check-workflow-conditions.ts#L67-L70)

### Attribute Definitions and Design Trade-offs

Workflow attributes define supported rule keys, input types, required data dependencies, and operator compatibility.

| Attribute Key | Label | Input Type | Allowed Operators | Data Requirements |
| :--- | :--- | :--- | :--- | :--- |
| `totalLeads` | total leads | number | `gte` | `partnerLinkStats` |
| `totalConversions` | total conversions | number | `gte` | `partnerLinkStats` |
| `totalSaleAmount` | total revenue | currency | `gte` | `partnerLinkStats` |
| `totalCommissions` | total commissions | currency | `gte` | `commissions` |
| `partnerEnrolledDays` | enrollment duration | dropdown | `gte` | none |
| `partnerJoined` | joins the program | none | `gte` | none |
| `partnerGroup` | group | group | `eq`, `ne`, `in`, `notIn` | none |

Sources: [apps/web/lib/api/workflows/attribute-definitions.ts:24-92](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workflows/attribute-definitions.ts#L24-L92)

| Design Choice | Benefit | Cost |
| :--- | :--- | :--- |
| Discriminated unions for actions | Strict type safety and clear payload narrowing per action type | Requires explicit type mapping updates when adding new actions |
| Superset attribute definitions | Centralizes attribute rules across UI and API layers | Couples attribute keys to specific data fetching requirements |
| Standalone schema validation | Decouples condition checks from HTTP request lifecycles | Requires duplicate error context mapping for user-facing validation |

Sources: [apps/web/lib/api/workflows/attribute-definitions.ts:8-92](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workflows/attribute-definitions.ts#L8-L92), [apps/web/lib/zod/schemas/workflows.ts:68-89](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/workflows.ts#L68-L89), [apps/web/lib/api/workflows/check-workflow-conditions.ts:8-92](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workflows/check-workflow-conditions.ts#L8-L92)

## Workflow Ingestion and Dispatch Pipeline

### Overview

The workflow ingestion and dispatch pipeline manages the serialization, queueing, and delivery of asynchronous workflow trigger events to their respective execution endpoints. Trigger events are initialized through helper routines like `queuePartnerCommissionCreation()`, which fetches program enrollment records and dispatches payload data to Upstash QStash via `triggerWorkflows()`.

Sources: [apps/web/lib/partners/queue-partner-commission-creation.ts:6-40](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/queue-partner-commission-creation.ts#L6-L40), [apps/web/lib/jobs/send-workflows.ts:82-136](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/send-workflows.ts#L82-L136)

### Workflow Trigger Dispatch Call Chain

The execution pipeline processes asynchronous events by translating persisted job records into HTTP dispatch requests sent to Upstash QStash.

1. `queuePartnerCommissionCreation()` — Validates partner enrollment and constructs the commission creation parameters.
2. `dispatchWorkflows()` — Passes the structured job definition containing the workflow name and payload.
3. `triggerWorkflows()` — Chunks incoming jobs according to batch size limits and invokes the QStash client.
4. `buildTriggerRequest()` — Maps job names to route paths and applies flow control parameters.

Sources: [apps/web/lib/partners/queue-partner-commission-creation.ts:6-40](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/queue-partner-commission-creation.ts#L6-L40), [apps/web/lib/jobs/send-workflows.ts:62-136](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/send-workflows.ts#L62-L136)

### Supported Workflow Endpoints and Routing

Workflow names must conform to a strict kebab-case schema ending in `-workflow` and map directly to specific API route destinations.

| Workflow Name | Route Path |
| :--- | :--- |
| `partner-approved-workflow` | `/api/workflows/partner-approved` |
| `merge-partner-accounts-workflow` | `/api/workflows/merge-partner-accounts` |
| `create-partner-commission-workflow` | `/api/workflows/create-partner-commission` |
| `reattribute-customer-workflow` | `/api/workflows/reattribute-customer` |
| `detach-discount-workflow` | `/api/workflows/detach-discount` |

Sources: [apps/web/lib/jobs/send-workflows.ts:20-34](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/send-workflows.ts#L20-L34)

> [!WARNING]
> Workflow names are validated against the regular expression `/^[a-z][a-z0-9]*(-[a-z0-9]+)*-workflow$/`. Names failing to match this pattern or omitting the required `-workflow` suffix fail schema parsing during initialization.

Sources: [apps/web/lib/jobs/send-workflows.ts:20-25](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/send-workflows.ts#L20-L25)

### Action Execution and Context Resolution

Once workflow events reach execution endpoints, `executeWorkflows()` evaluates trigger conditions against aggregated partner metrics and dispatches actions through the `ACTION_HANDLERS` dictionary.

| Action Type | Handler Function |
| :--- | :--- |
| `AwardBounty` | `executeAwardBountyWorkflow` |
| `SendCampaign` | `executeSendCampaignWorkflow` |
| `MoveGroup` | `executeMoveGroupWorkflow` |

Sources: [apps/web/lib/api/workflows/execute-workflows.ts:23-35](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workflows/execute-workflows.ts#L23-L35)

> [!TIP]
> Commission aggregations are evaluated lazily. The execution engine inspects workflow configuration conditions and skips expensive database aggregate queries unless `totalCommissions` is explicitly required by active rules.

Sources: [apps/web/lib/api/workflows/execute-workflows.ts:111-168](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workflows/execute-workflows.ts#L111-L168)

### Pipeline Design Trade-offs

| Design Choice | Benefit | Cost |
| :--- | :--- | :--- |
| Static workflow path mapping | Ensures strict compile-time validation between job names and HTTP endpoints | Requires manual registry updates when adding new workflow routes |
| Lazy commission aggregation | Avoids costly database queries when rules do not evaluate commission metrics | Introduces conditional branching complexity inside data loading promises |
| QStash chunked batching | Prevents payload size limit violations by splitting bulk dispatches | Requires error handling per chunk rather than individual transactions |

Sources: [apps/web/lib/api/workflows/execute-workflows.ts:111-168](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workflows/execute-workflows.ts#L111-L168), [apps/web/lib/jobs/send-workflows.ts:27-34](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/send-workflows.ts#L27-L34), [apps/web/lib/jobs/send-workflows.ts:90-131](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/send-workflows.ts#L90-L131)

## Partner Commission Creation Pipeline

### Overview

Partner commission generation handles custom, lead, and sale rewards through a multi-step asynchronous pipeline. When a conversion event occurs, `queuePartnerCommissionCreation()` verifies partner enrollment and dispatches the payload to the `create-partner-commission-workflow` queue with flow control parallelism configured to `1` keyed by `partnerId`.

Sources: [apps/web/lib/partners/queue-partner-commission-creation.ts:6-40](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/queue-partner-commission-creation.ts#L6-L40), [apps/web/lib/openapi/commissions/create-commission.ts:1-35](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/openapi/commissions/create-commission.ts#L1-L35)

### Step-by-Step Commission Execution Walkthrough

The core commission generation process flows through several sequential verification and calculation steps inside `stepCreateCommission()`:

1. `stepCreateCommission()` — Receives the workflow input, normalizes missing numeric amounts to `0`, and filters out invalid raw event types such as `click` or `referral`.
2. `determinePartnerRewards()` — Evaluates program enrollment status, context, quantity, and link identifiers to match applicable reward rules and set initial earnings or reward configurations.
3. `prisma.commission.findFirst()` — Queries prior commission history for the specific partner and customer combination to determine whether the event is a new conversion or a recurring sale.
4. Fraud and Duplication Checks — Validates that previous commissions are not marked as `fraud` or `canceled`, prevents duplicate lead commissions for the same customer, and verifies subscription duration against `maxDuration` limits.
5. `executeSideEffects()` — Updates link and customer statistics within a Prisma transaction and invokes secondary asynchronous side effects like partner link stats synchronization and workflow executions.

Sources: [apps/web/app/ee/api/workflows/create-partner-commission/route.ts:182-391](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L182-L391), [apps/web/lib/api/commissions/create-manual-commissions.ts:741-873](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/create-manual-commissions.ts#L741-L873)

> [!CAUTION]
> If a partner's prior first commission has a status of `fraud` or `canceled`, commission creation is immediately aborted to prevent cascading payouts on compromised customer accounts.

Sources: [apps/web/app/ee/api/workflows/create-partner-commission/route.ts:313-319](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L313-L319)

### Pipeline Design Trade-offs

| Design Choice | Benefit | Cost |
| :--- | :--- | :--- |
| Serialized workflow execution (`parallelism: 1`) | Prevents race conditions and duplicate commission entries for the same partner | Serializes concurrent conversions for a single partner, potentially increasing queue latency |
| Historical commission lookups | Enables accurate recurring sale attribution and max-duration checks | Adds database query overhead to every lead and sale evaluation |
| Dual Redis caching for lead events | Supports rapid deduplication and immediate access before Tinybird ingestion | Requires managing separate cache keys with expiration windows |

Sources: [apps/web/lib/partners/queue-partner-commission-creation.ts:25-31](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/queue-partner-commission-creation.ts#L25-L31), [apps/web/app/ee/api/workflows/create-partner-commission/route.ts:243-258](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L243-L258), [apps/web/lib/api/conversions/track-lead.ts:91-106](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/conversions/track-lead.ts#L91-L106), [apps/web/lib/api/conversions/track-lead.ts:210-223](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/conversions/track-lead.ts#L210-L223)

## Partner Approval and Enrollment Workflows

### Overview

Partner approval and enrollment workflows orchestrate the onboarding lifecycle when a partner application is accepted into a program. The orchestration executes through `POST /api/workflows/partner-approved`, fetching program enrollment details and running six sequential actions ranging from default link creation to email notifications, webhook dispatch, bounty draft upserts, and Dub workflow executions.

Sources: [apps/web/app/ee/api/workflows/partner-approved/route.ts:34-51](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/partner-approved/route.ts#L34-L51), [apps/web/app/ee/api/workflows/partner-approved/route.ts:53-72](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/partner-approved/route.ts#L53-L72)

### Partner Approval Execution Walkthrough

The partner approval handler coordinates several discrete steps within workflow contexts:

1. `getProgramEnrollmentOrThrow()` — Retrieves enrollment records including program, partner, and existing links using `programId` and `partnerId`.
2. `context.run("create-default-links", ...)` — Verifies partner group associations, fetches default links and UTM templates from `prisma.partnerGroup`, filters out already created links, and invokes `createPartnerDefaultLinks()`.
3. `context.run("create-discount-codes", ...)` — Fetches workspace configuration and calls `generateDiscountCodeForPartner()` if auto-provisioning is enabled.
4. `context.run("send-email", ...)` — Queries `prisma.partnerUser` for users opted into `applicationApproved` notifications, fetches reward configurations via `getGroupRewardsAndBounties()`, and dispatches batch emails using `sendBatchEmail()`.
5. `context.run("send-webhook", ...)` — Queries partner platforms via `prisma.partnerPlatform`, normalizes social media fields with `polyfillSocialMediaFields()`, and parses the enrolled partner schema.
6. Cron upsert job triggering — Invokes scheduled draft bounty submission upserts for performance-based bounties via QStash publishing when partner enrollments match commission-eligible statuses.

Sources: [apps/web/app/ee/api/workflows/partner-approved/route.ts:59-293](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/partner-approved/route.ts#L59-L293), [apps/web/app/ee/api/cron/bounties/upsert-draft-submissions/route.ts:235-244](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/bounties/upsert-draft-submissions/route.ts#L235-L244)

> [!NOTE]
> Network programs designated by `NETWORK_PROGRAM_ID` short-circuit the enrollment workflow immediately after step 1, restricting execution solely to default link creation.

Sources: [apps/web/app/ee/api/workflows/partner-approved/route.ts:170-174](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/partner-approved/route.ts#L170-L174)

### Bounty Draft Submission Schema and Parameters

Cron jobs handling lifetime performance bounties parse request bodies against a strict Zod schema and query program enrollments with specific filters and pagination limits.

| Parameter | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `bountyId` | `string` (required) | — | Unique identifier of the bounty being processed |
| `partnerIds` | `string[]` (optional) | `undefined` | Optional array of specific partner identifiers to filter scope |
| `page` | `number` (optional) | `0` | Pagination page offset for batching enrollment processing |

Sources: [apps/web/app/ee/api/cron/bounties/upsert-draft-submissions/route.ts:20-26](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/bounties/upsert-draft-submissions/route.ts#L20-L26), [apps/web/app/ee/api/cron/bounties/upsert-draft-submissions/route.ts:39-143](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/bounties/upsert-draft-submissions/route.ts#L39-L143)

> [!WARNING]
> If a bounty has not started yet and the time difference is 10 minutes or greater, or if the bounty type is not performance-based, submission creation is aborted with an early response.

Sources: [apps/web/app/ee/api/cron/bounties/upsert-draft-submissions/route.ts:68-80](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/bounties/upsert-draft-submissions/route.ts#L68-L80)

## Customer Reattribution and Event Replay

### Customer Reattribution and Event Replay Overview

Customer reattribution transfers a customer and their historical tracking events and commissions from an old partner link to a new one. The workflow is orchestrated by `POST /api/workflows/reattribute-customer` via Upstash Workflow and executes seven sequential action steps ranging from event plan analysis and Tinybird event re-ingestion to link stat reconciliation, commission transfers, clawback generation, old event deletion, and old link stat decrementing.

Sources: [apps/web/app/ee/api/workflows/reattribute-customer/route.ts:21-33](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/reattribute-customer/route.ts#L21-L33), [apps/web/app/ee/api/workflows/reattribute-customer/route.ts:36-176](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/reattribute-customer/route.ts#L36-L176)

### Customer Reattribution Execution Walkthrough

The reattribution pipeline coordinates state adjustments across both analytical data stores (Tinybird) and relational records (Prisma) through the following call chain:

1. `load-plan` (`loadReattributeEventPlan()`) — Inspects old and new customer events in Tinybird to construct the reattribution event plan.
2. `reingest-events` (`reingestCustomerEvents()`) — Queries `prisma.link` using `newLinkId` and re-ingests events onto the new customer and link, handling `WorkflowRetryAfterError` with a 5-second backoff for transient failures.
3. `increment-new-link-stats` (`incrementNewLinkStats()`) — Updates metrics for the new link using the loaded reattribution plan and increment options.
4. `transfer-unpaid-commissions` (`transferUnpaidCommissions()`) — Moves pending, hold, and processed commissions from the old partner context to the new partner context.
5. `load-clawback-plan` & `optional-clawback` (`loadClawbackPlan()` & `applyClawbackAndReplacementCommissions()`) — Optionally calculates clawbacks for paid earnings on the old partner, clears invoice/event identifiers if required, queues negative tracking error commissions, and recreates valid lead or sale replacement commissions under the new partner.
6. `delete-old-events` (`deleteTinybirdCustomerEvents()`) — Purges historical customer events from Tinybird for `oldCustomerId`, throwing a `WorkflowRetryAfterError` with a 10-second backoff upon failure.
7. `decrement-old-link-stats` (`decrementOldLinkStats()`) — Decrements statistics and conversions on the old link based on the loaded plan.

Sources: [apps/web/app/ee/api/workflows/reattribute-customer/route.ts:40-176](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/reattribute-customer/route.ts#L40-L176)

### Reattribution Workflow Parameters and Constants

The customer reattribution engine relies on specific limits, status categories, and schema configurations defined across the API library and workflow routes.

| Constant / Parameter | Type / Value | Description |
| :--- | :--- | :--- |
| `CUSTOMER_REATTRIBUTION_EVENTS_LIMIT` | `number` (`500`) | Maximum number of events processed during customer reattribution |
| `UNPAID_COMMISSION_STATUSES` | `readonly string[]` (`["pending", "hold", "processed"]`) | Commission statuses eligible for direct transfer during reattribution |
| `STATS_LOCK_TTL_SECONDS` | `number` (`86400`) | Time-to-live in seconds for statistics synchronization locks (24 hours) |
| `WorkflowRetryAfterError` (reingest) | `Error`, `"5s"` backoff | Exception thrown to retry event re-ingestion on failure |
| `WorkflowRetryAfterError` (delete) | `Error`, `"10s"` backoff | Exception thrown to retry Tinybird event deletion on failure |

Sources: [apps/web/lib/api/customers/reattribute-customer.ts:22-24](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/customers/reattribute-customer.ts#L22-L24), [apps/web/app/ee/api/workflows/reattribute-customer/route.ts:77-80](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/reattribute-customer/route.ts#L77-L80), [apps/web/app/ee/api/workflows/reattribute-customer/route.ts:156-161](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/reattribute-customer/route.ts#L156-L161)

### Reattribution Design Trade-Offs

| Design Choice | Benefit | Cost |
| :--- | :--- | :--- |
| Asynchronous multi-step workflow execution via Upstash | Ensures durable retries and decoupled execution for long-running analytics re-ingestion | Increases total completion latency due to step boundary serialization |
| Distributed run locking (`runOnce`) on clawback calculations | Prevents duplicate clawback generation and race conditions on financial adjustments | Requires persistent coordination state via Redis/Prisma |
| Event re-ingestion followed by old event deletion | Avoids temporary data loss windows during customer context switching | Briefly duplicates analytical event counts across partners during migration |

Sources: [apps/web/lib/api/customers/reattribute-customer.ts:741-771](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/customers/reattribute-customer.ts#L741-L771), [apps/web/app/ee/api/workflows/reattribute-customer/route.ts:47-163](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/reattribute-customer/route.ts#L47-L163)

> [!WARNING]
> If a re-ingestion attempt encounters an explicit `ZodError` or an error message containing `"too many events to reattribute"`, the workflow immediately rejects and bypasses automatic retry handlers.

Sources: [apps/web/app/ee/api/workflows/reattribute-customer/route.ts:68-76](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/reattribute-customer/route.ts#L68-L76)

## Discount Detachment and Remapping

### Overview

The discount detachment workflow handles the asynchronous disassociation of discounts from program enrollments, link rewards, and code remapping. Soft-deleted discounts with their `programId` cleared are cleaned up through a structured pipeline that runs disassociations in parallel before initiating code remapping. Hard-deletion is deferred to an orphaned cleanup cron route once remapping finishes and no remaining entities reference the soft-deleted discount.

Sources: [apps/web/app/ee/api/workflows/detach-discount/route.ts:19-30](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/detach-discount/route.ts#L19-L30)

### Workflow Execution Call-Chain and Steps

The `POST` handler exposed by `@upstash/workflow/nextjs` processes input matching `inputSchema` (`programId` and `discountId` string fields) through several ordered execution blocks.

1. `validate-discount` (`prisma.discount.findUnique`) — Queries the database for the discount by `discountId`, selecting `id` and `programId`. If the discount is not found or `programId` is not `null`, the workflow returns early and skips execution.
2. `detach-discount-from-enrollments` & `detach-discount-from-link-rewards` (`Promise.all`) — Runs `detachDiscountFromProgramEnrollments()` and `detachDiscountFromLinkRewards()` concurrently using `context.run`.
3. `remap-discount-codes` (`dispatchRemapDiscountCodes`) — Dispatches remapping jobs for discount codes associated with the discount after enrollments and link rewards are fully updated.

Sources: [apps/web/app/ee/api/workflows/detach-discount/route.ts:12-107](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/detach-discount/route.ts#L12-L107)

> [!WARNING]
> If a discount is queried during validation and its `programId` is still present (not `null`), the workflow halts execution and skips the detachment pipeline.

Sources: [apps/web/app/ee/api/workflows/detach-discount/route.ts:55-60](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/detach-discount/route.ts#L55-L60)

### Failure Handling and Logging

When workflow execution encounters an unhandled exception or failure, the Upstash workflow invokes the configured `failureFunction`. This captures context metadata and flushes error details through Axiom logging.

```typescript
failureFunction: async ({
  context,
  failStatus,
  failResponse,
  failHeaders,
}) => {
  logger.error("workflow.failed", {
    service: "qstash",
    event: "workflow.failed",
    workflowType: "detach-discount",
    workflowRunId: context.workflowRunId,
    discountId: context.requestPayload?.discountId,
    programId: context.requestPayload?.programId,
    failStatus,
    failResponse,
    failHeaders,
  });

  await logger.flush();
},
```

Sources: [apps/web/app/ee/api/workflows/detach-discount/route.ts:109-130](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/detach-discount/route.ts#L109-L130)

> [!NOTE]
> Hard-deletion of the discount record is never performed directly within this workflow; it is delegated entirely to `/api/cron/cleanup/orphaned` after all references have been successfully disassociated.

Sources: [apps/web/app/ee/api/workflows/detach-discount/route.ts:27-29](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/detach-discount/route.ts#L27-L29)

## Referral Commissions and Backfill Batches

### Overview

The referral commission pipeline handles scheduled cron jobs and batch workers that evaluate referral reward triggers, enforce duration caps, verify commission thresholds, and execute historical backfills for partner programs.

Sources: [apps/web/app/ee/api/cron/commissions/referrals/create/route.ts:1-26](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/commissions/referrals/create/route.ts#L1-L26), [apps/web/app/ee/api/cron/commissions/referrals/backfill/route.ts:1-135](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/commissions/referrals/backfill/route.ts#L1-L135)

### Referral Commission Execution Walkchain

The referral commission generation process flows through specific validation and calculation functions when triggered via cron:

1. `POST` (`/api/cron/commissions/referrals/create`) — Parses the incoming raw request body using `inputSchema` (accepting either `{ sourceCommissionId }` or `{ programId, partnerId }`).
2. `createReferralCommission` (`/lib/partner-referrals/create-referral-commission.ts`) — Resolves referral context, checks for self-referrals (`partnerId === referredByPartnerId`), ignores the network program ID (`NETWORK_PROGRAM_ID`), and retrieves the referrer's `ProgramEnrollment` and `referralReward`.
3. `referralRewardConfigSchema.safeParse` — Validates the reward configuration structure and extracts the `trigger` and `commissionsThresholdInCents`.
4. Trigger Evaluation & Calculation — Computes earnings based on the specific reward trigger type (`commissionEarned`, `saleRecorded`, `partnerApproved`, or `commissionThreshold`), checking `maxDuration` constraints via `differenceInMonths` when applicable.
5. `prisma.commission.create` — Persists the new referral commission with `type: CommissionType.referral` and a unique `invoiceId`.

Sources: [apps/web/app/ee/api/cron/commissions/referrals/create/route.ts:8-17](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/commissions/referrals/create/route.ts#L8-L17), [apps/web/lib/partner-referrals/create-referral-commission.ts:18-237](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partner-referrals/create-referral-commission.ts#L18-L237)

### Referral Reward Triggers and Configuration

Referral rewards support distinct triggers defined in `referralRewardConfigSchema`.

| Trigger Identifier | Required Configuration | Behavior & Evaluation |
| :--- | :--- | :--- |
| `commissionEarned` | `trigger` | Calculates referral earnings as a percentage of the source commission's earnings. |
| `saleRecorded` | `trigger` | Calculates referral earnings as a percentage of the source commission's sale amount. |
| `partnerApproved` | `trigger` | Awards a flat amount (`amountInCents`) upon partner approval. |
| `commissionThreshold` | `trigger`, `commissionsThresholdInCents` | Aggregates earned sale commissions for the partner; grants reward if total meets or exceeds `commissionsThresholdInCents`. |

Sources: [apps/web/lib/partner-referrals/create-referral-commission.ts:99-190](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partner-referrals/create-referral-commission.ts#L99-L190), [apps/web/lib/zod/schemas/rewards.ts:495-511](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/rewards.ts#L495-L511)

> [!WARNING]
> Self-referrals where `partnerId` matches `referredByPartnerId` and creation requests targeting the network program (`NETWORK_PROGRAM_ID`) are explicitly intercepted and skipped.

Sources: [apps/web/lib/partner-referrals/create-referral-commission.ts:30-42](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partner-referrals/create-referral-commission.ts#L30-L42)

### Historical Backfill Pipeline

The backfill cron route (`/api/cron/commissions/referrals/backfill`) queries `ProgramApplicationEvent` and `ProgramEnrollment` to check historical events for a partner.

- For `commissionThreshold` and `partnerApproved` triggers, it enqueues a single batch job to `/api/cron/commissions/referrals/create`.
- For `saleRecorded` and `commissionEarned` triggers, it fetches up to 50 eligible sale commissions at a time (`status` in `pending`, `processed`, `paid`) and enqueues individual creation jobs for each source commission ID via `enqueueBatchJobs` and `chunk`.

Sources: [apps/web/app/ee/api/cron/commissions/referrals/backfill/route.ts:12-130](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/commissions/referrals/backfill/route.ts#L12-L130)

> [!IMPORTANT]
> If a referrer's reward configuration uses an unsupported or unrecognized trigger, the backfill endpoint returns early with a skip response.

Sources: [apps/web/app/ee/api/cron/commissions/referrals/backfill/route.ts:132-135](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/commissions/referrals/backfill/route.ts#L132-L135)

## Related

- [Background Jobs and Queues](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/core-architecture/background-jobs-and-queues)
- [Commission Rules and Rewards](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/affiliate-platform/commission-rules-and-rewards)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/llms.txt) for all pages in this wiki.
