Getting Started
Core Architecture
Link Engine
Analytics & Attribution
Partners & Affiliates
Third-Party Integrations
Identity & Security
Automation & Messaging
Developer Tools
The following files were used as context for generating this wiki page:
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, apps/web/lib/api/workflows/execute-workflows.ts:1-43, apps/web/lib/jobs/send-workflows.ts:1-80
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.
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].
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.
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(),
}),
}),
]);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.
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 };
}Warning
Attributes with an input type of "none", such as partnerJoined, intentionally skip value validation checks and exit early during condition evaluation.
Workflow attributes define supported rule keys, input types, required data dependencies, and operator compatibility.
Sources: apps/web/lib/api/workflows/attribute-definitions.ts:8-92, apps/web/lib/zod/schemas/workflows.ts:68-89, apps/web/lib/api/workflows/check-workflow-conditions.ts:8-92
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, apps/web/lib/jobs/send-workflows.ts:82-136
The execution pipeline processes asynchronous events by translating persisted job records into HTTP dispatch requests sent to Upstash QStash.
queuePartnerCommissionCreation() — Validates partner enrollment and constructs the commission creation parameters.dispatchWorkflows() — Passes the structured job definition containing the workflow name and payload.triggerWorkflows() — Chunks incoming jobs according to batch size limits and invokes the QStash client.buildTriggerRequest() — Maps job names to route paths and applies flow control parameters.Sources: apps/web/lib/partners/queue-partner-commission-creation.ts:6-40, apps/web/lib/jobs/send-workflows.ts:62-136
Workflow names must conform to a strict kebab-case schema ending in -workflow and map directly to specific API route destinations.
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.
Once workflow events reach execution endpoints, executeWorkflows() evaluates trigger conditions against aggregated partner metrics and dispatches actions through the ACTION_HANDLERS dictionary.
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, apps/web/lib/jobs/send-workflows.ts:27-34, apps/web/lib/jobs/send-workflows.ts:90-131
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, apps/web/lib/openapi/commissions/create-commission.ts:1-35
The core commission generation process flows through several sequential verification and calculation steps inside stepCreateCommission():
stepCreateCommission() — Receives the workflow input, normalizes missing numeric amounts to 0, and filters out invalid raw event types such as click or referral.determinePartnerRewards() — Evaluates program enrollment status, context, quantity, and link identifiers to match applicable reward rules and set initial earnings or reward configurations.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.fraud or canceled, prevents duplicate lead commissions for the same customer, and verifies subscription duration against maxDuration limits.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, apps/web/lib/api/commissions/create-manual-commissions.ts:741-873
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/lib/partners/queue-partner-commission-creation.ts:25-31, apps/web/app/ee/api/workflows/create-partner-commission/route.ts:243-258, apps/web/lib/api/conversions/track-lead.ts:91-106, apps/web/lib/api/conversions/track-lead.ts:210-223
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, apps/web/app/ee/api/workflows/partner-approved/route.ts:53-72
The partner approval handler coordinates several discrete steps within workflow contexts:
getProgramEnrollmentOrThrow() — Retrieves enrollment records including program, partner, and existing links using programId and partnerId.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().context.run("create-discount-codes", ...) — Fetches workspace configuration and calls generateDiscountCodeForPartner() if auto-provisioning is enabled.context.run("send-email", ...) — Queries prisma.partnerUser for users opted into applicationApproved notifications, fetches reward configurations via getGroupRewardsAndBounties(), and dispatches batch emails using sendBatchEmail().context.run("send-webhook", ...) — Queries partner platforms via prisma.partnerPlatform, normalizes social media fields with polyfillSocialMediaFields(), and parses the enrolled partner schema.Sources: apps/web/app/ee/api/workflows/partner-approved/route.ts:59-293, apps/web/app/ee/api/cron/bounties/upsert-draft-submissions/route.ts:235-244
Note
Network programs designated by NETWORK_PROGRAM_ID short-circuit the enrollment workflow immediately after step 1, restricting execution solely to default link creation.
Cron jobs handling lifetime performance bounties parse request bodies against a strict Zod schema and query program enrollments with specific filters and pagination limits.
Sources: apps/web/app/ee/api/cron/bounties/upsert-draft-submissions/route.ts:20-26, apps/web/app/ee/api/cron/bounties/upsert-draft-submissions/route.ts:39-143
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.
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, apps/web/app/ee/api/workflows/reattribute-customer/route.ts:36-176
The reattribution pipeline coordinates state adjustments across both analytical data stores (Tinybird) and relational records (Prisma) through the following call chain:
load-plan (loadReattributeEventPlan()) — Inspects old and new customer events in Tinybird to construct the reattribution event plan.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.increment-new-link-stats (incrementNewLinkStats()) — Updates metrics for the new link using the loaded reattribution plan and increment options.transfer-unpaid-commissions (transferUnpaidCommissions()) — Moves pending, hold, and processed commissions from the old partner context to the new partner context.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.delete-old-events (deleteTinybirdCustomerEvents()) — Purges historical customer events from Tinybird for oldCustomerId, throwing a WorkflowRetryAfterError with a 10-second backoff upon failure.decrement-old-link-stats (decrementOldLinkStats()) — Decrements statistics and conversions on the old link based on the loaded plan.The customer reattribution engine relies on specific limits, status categories, and schema configurations defined across the API library and workflow routes.
Sources: apps/web/lib/api/customers/reattribute-customer.ts:22-24, apps/web/app/ee/api/workflows/reattribute-customer/route.ts:77-80, apps/web/app/ee/api/workflows/reattribute-customer/route.ts:156-161
Sources: apps/web/lib/api/customers/reattribute-customer.ts:741-771, apps/web/app/ee/api/workflows/reattribute-customer/route.ts:47-163
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.
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.
The POST handler exposed by @upstash/workflow/nextjs processes input matching inputSchema (programId and discountId string fields) through several ordered execution blocks.
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.detach-discount-from-enrollments & detach-discount-from-link-rewards (Promise.all) — Runs detachDiscountFromProgramEnrollments() and detachDiscountFromLinkRewards() concurrently using context.run.remap-discount-codes (dispatchRemapDiscountCodes) — Dispatches remapping jobs for discount codes associated with the discount after enrollments and link rewards are fully updated.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.
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.
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();
},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.
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, apps/web/app/ee/api/cron/commissions/referrals/backfill/route.ts:1-135
The referral commission generation process flows through specific validation and calculation functions when triggered via cron:
POST (/api/cron/commissions/referrals/create) — Parses the incoming raw request body using inputSchema (accepting either { sourceCommissionId } or { programId, partnerId }).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.referralRewardConfigSchema.safeParse — Validates the reward configuration structure and extracts the trigger and commissionsThresholdInCents.commissionEarned, saleRecorded, partnerApproved, or commissionThreshold), checking maxDuration constraints via differenceInMonths when applicable.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, apps/web/lib/partner-referrals/create-referral-commission.ts:18-237
Referral rewards support distinct triggers defined in referralRewardConfigSchema.
Sources: apps/web/lib/partner-referrals/create-referral-commission.ts:99-190, apps/web/lib/zod/schemas/rewards.ts:495-511
Warning
Self-referrals where partnerId matches referredByPartnerId and creation requests targeting the network program (NETWORK_PROGRAM_ID) are explicitly intercepted and skipped.
The backfill cron route (/api/cron/commissions/referrals/backfill) queries ProgramApplicationEvent and ProgramEnrollment to check historical events for a partner.
commissionThreshold and partnerApproved triggers, it enqueues a single batch job to /api/cron/commissions/referrals/create.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.Important
If a referrer's reward configuration uses an unsupported or unrecognized trigger, the backfill endpoint returns early with a skip response.