-
Notifications
You must be signed in to change notification settings - Fork 460
Workflows #873
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. Caution Review failedThe pull request is closed. WalkthroughAdds multi-tenant workflow orchestration: DB models + migration, compile/execute runtime via Freestyle, workflow-token auth and CORS header, dashboard UI for managing workflows and editing TS source, config schema and defaults, user-signup trigger wiring, E2E tests, and several utility/interface adjustments. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant API as Users CRUD (API)
participant WF as triggerWorkflows()
participant COMP as compileAndGetEnabledWorkflows()
participant DB as Prisma (Workflows*)
participant RT as Freestyle Runtime
U->>API: Sign up / upgrade from anonymous
API->>WF: triggerWorkflows(tenancy, {type:"sign-up", ...})
WF->>COMP: load enabled workflows for tenancy
COMP->>DB: read config, compiled artifacts
alt not compiled or stale
COMP->>RT: bundle & initialize compiled code
RT-->>COMP: Result {compiledCode, registeredTriggers} / {compileError}
COMP->>DB: persist CompiledWorkflow / compile error
end
WF->>DB: create WorkflowExecution
par For each workflow registering "sign-up"
WF->>RT: execute trigger with env + data
RT-->>WF: Result {output} / {error}
WF->>DB: persist WorkflowTrigger (output or error)
end
sequenceDiagram
autonumber
actor C as Client
participant SR as Smart Request
participant DB as Prisma (WorkflowTriggerToken)
note over C,SR: Request with header x-stack-workflow-token
C->>SR: Request + x-stack-workflow-token
alt Admin request
SR-->>C: 400 KnownErrors.AdminAuthenticationRequired
else Client/Server request
SR->>SR: hashWorkflowTriggerToken(token)
SR->>DB: find token by tenancyId + tokenHash
alt Token not found
SR-->>C: 400 KnownErrors.WorkflowTokenDoesNotExist
else Token expired
SR-->>C: 400 KnownErrors.WorkflowTokenExpired
else Valid
SR-->>C: Proceed (authenticated via workflow token)
end
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (2)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Greptile Summary
This PR introduces a comprehensive workflows feature to Stack Auth that allows users to create, manage, and execute custom TypeScript workflows. The implementation spans multiple layers of the application:
Core Infrastructure Changes:
- New Prisma schema models (
WorkflowTrigger
,WorkflowExecution
,CurrentlyCompilingWorkflow
,CompiledWorkflow
) establish a complete workflow lifecycle system with compilation versioning, execution tracking, and trigger management - Enhanced environment variable support in the Stack SDK template to work beyond Next.js applications by adding fallback environment variables without the
NEXT_PUBLIC_
prefix - Updated configuration schema to support workflow definitions with TypeScript source code, display names, and enabled/disabled states
Workflow Engine:
- New workflow compilation and execution system using esbuild for TypeScript compilation and Freestyle sandboxes for secure code execution
- Trigger registration system for events like user sign-ups with proper database persistence
- Result-based error handling pattern adoption across the Freestyle integration
Dashboard UI:
- Complete CRUD interface for workflow management including list view, individual workflow editing pages, search functionality, and workflow state management (enabled/disabled)
- Code editor interface with syntax highlighting for editing TypeScript workflow source code
- Integration with existing project configuration system for persistence
Supporting Changes:
- New type guard utilities for runtime array validation
- Enhanced timeout function to support lazy promise evaluation
- Updated email rendering to use the new Freestyle API structure
- Cosmetic improvement changing "Payments" to "Offers & Items" in the dashboard for better clarity
The workflows feature enables users to define custom automation and business logic in TypeScript that can be triggered by authentication events, extending Stack Auth's capabilities beyond basic authentication to include custom workflows and integrations.
Confidence score: 1/5
- This PR has critical implementation issues that will cause immediate problems in production, including broken number formatting and incomplete core functionality
- Score severely lowered due to completely broken
prettyPrintWithMagnitudes
function, unimplemented coregetCompiledWorkflows
function, and hardcoded placeholder credentials in workflow execution - Pay immediate attention to
packages/stack-shared/src/utils/numbers.tsx
andapps/backend/src/lib/workflows.tsx
which contain blocking issues that must be fixed before merge
13 files reviewed, 6 comments
|
||
const handleSubmit = async () => { | ||
if (!displayName) { | ||
alert("Please fill in all required fields"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Using alert()
instead of consistent toast notifications
alert("Please fill in all required fields"); | |
toast({ title: "Please fill in all required fields", variant: "destructive" }); |
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx
Show resolved
Hide resolved
const magnitudes = [ | ||
[1_000_000_000_000_000, "trln"], | ||
[1_000_000_000_000, "bln"], | ||
[1_000_000_000, "bn"], | ||
[1_000_000, "M"], | ||
[1_000, "k"], | ||
[1_000, "M"], | ||
[1_000, "bn"], | ||
[1_000, "bln"], | ||
[1_000, "trln"], | ||
] as const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Critical error: All magnitude values are set to 1,000 instead of their correct values (1M, 1B, 1T, etc.). This will break the conversion logic completely.
const magnitudes = [ | |
[1_000_000_000_000_000, "trln"], | |
[1_000_000_000_000, "bln"], | |
[1_000_000_000, "bn"], | |
[1_000_000, "M"], | |
[1_000, "k"], | |
[1_000, "M"], | |
[1_000, "bn"], | |
[1_000, "bln"], | |
[1_000, "trln"], | |
] as const; | |
const magnitudes = [ | |
[1_000, "k"], | |
[1_000_000, "M"], | |
[1_000_000_000, "bn"], | |
[1_000_000_000_000, "bln"], | |
[1_000_000_000_000_000, "trln"], | |
] as const; |
const availableWorkflows = config.workflows.availableWorkflows; | ||
const workflow = workflowId in availableWorkflows ? availableWorkflows[workflowId] : undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Consider using Map<string, WorkflowType>
instead of plain object access to avoid potential prototype pollution vulnerabilities when using dynamic keys
Context Used: Rule - Use Map<A, B> instead of plain objects when using dynamic keys to avoid prototype pollution vulnerabilities. (link)
Review by RecurseML🔍 Review performed on 1e06ff4..694425c ✨ No bugs found, your code is sparkling clean ✅ Files analyzed, no issues (5)• ⏭️ Files skipped (low suspicion) (8)• |
Documentation Changes Required1. docs/templates/getting-started/setup.mdx
2. docs/templates/snippets/stack-app-constructor-options-before-ssk.mdx
Please ensure these changes are reflected in the relevant documentation files to maintain consistency with the recent updates. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR introduces a workflow automation system to Stack Auth, adding the ability to create, manage, and execute TypeScript-based workflows within projects. The implementation includes workflow compilation, execution infrastructure, database schema updates, and comprehensive UI components for workflow management.
- Adds workflow automation capabilities with TypeScript source code compilation and execution
- Implements workflow configuration schema, database models, and UI management components
- Updates environment variable handling to support fallback patterns for better configuration flexibility
Reviewed Changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
File | Description |
---|---|
packages/template/src/lib/stack-app/apps/implementations/common.ts | Adds fallback environment variables for configuration values |
packages/stack-shared/src/utils/promises.tsx | Enhances timeout function to accept functions that return promises |
packages/stack-shared/src/utils/numbers.tsx | Fixes magnitude calculation algorithm and adds comprehensive test coverage |
packages/stack-shared/src/utils/arrays.tsx | Adds type guard functions for array validation |
packages/stack-shared/src/config/schema.ts | Adds workflow configuration schema and updates error handling |
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/*.tsx | Implements workflow management UI components |
apps/backend/src/lib/workflows.tsx | Core workflow compilation and execution logic |
apps/backend/src/lib/freestyle.tsx | Updates freestyle sandbox integration for workflow execution |
apps/backend/src/lib/email-rendering.tsx | Updates email rendering to use improved freestyle API |
apps/backend/prisma/schema.prisma | Adds database models for workflow storage and execution tracking |
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx | Minor UI title update |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
[1_000, "M"], | ||
[1_000, "bn"], | ||
[1_000, "bln"], | ||
[1_000, "trln"], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All magnitude values are incorrectly set to 1_000. The values should be [1_000, "k"], [1_000_000, "M"], [1_000_000_000, "bn"], [1_000_000_000_000, "bln"], [1_000_000_000_000_000, "trln"] to represent the correct numerical thresholds for each suffix.
[1_000, "M"], | |
[1_000, "bn"], | |
[1_000, "bln"], | |
[1_000, "trln"], | |
[1_000_000, "M"], | |
[1_000_000_000, "bn"], | |
[1_000_000_000_000, "bln"], | |
[1_000_000_000_000_000, "trln"], |
Copilot uses AI. Check for mistakes.
|
||
const handleSubmit = async () => { | ||
if (!displayName) { | ||
alert("Please fill in all required fields"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace alert() with a toast notification for better user experience. The rest of the codebase consistently uses toast() for user feedback.
alert("Please fill in all required fields"); | |
toast("Please fill in all required fields"); |
Copilot uses AI. Check for mistakes.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx
Show resolved
Hide resolved
@@ -855,7 +876,7 @@ export async function getConfigOverrideErrors<T extends yup.AnySchema>(schema: T | |||
} | |||
export async function assertNoConfigOverrideErrors<T extends yup.AnySchema>(schema: T, config: unknown, options: { allowPropertiesThatCanNoLongerBeOverridden?: boolean, extraInfo?: any } = {}): Promise<void> { | |||
const res = await getConfigOverrideErrors(schema, config, options); | |||
if (res.status === "error") throw new StackAssertionError(`Config override is invalid — at a place where it should have already been validated! ${res.error}`, { options, config, schema }); | |||
if (res.status === "error") throw new StackAssertionError(`Config override is invalid — at a place where it should have already been validated! ${res.error}`, { options, config }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The schema
parameter was removed from the error context, but it could be valuable for debugging configuration validation issues. Consider keeping it or documenting why it was removed.
if (res.status === "error") throw new StackAssertionError(`Config override is invalid — at a place where it should have already been validated! ${res.error}`, { options, config }); | |
if (res.status === "error") throw new StackAssertionError(`Config override is invalid — at a place where it should have already been validated! ${res.error}`, { options, config, schema }); |
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
🧹 Nitpick comments (23)
packages/stack-shared/src/config/schema.ts (1)
143-152
: Workflows schema: guard against empty stringsConsider ensuring displayName and tsSource aren’t empty to avoid silent misconfigs.
Apply:
- displayName: yupString(), - tsSource: yupString(), + displayName: yupString().trim().min(1), + tsSource: yupString().trim().min(1),Will users commonly toggle only “enabled” via overrides without setting displayName/tsSource? If yes, confirm that warning handling remains acceptable when these fields are missing at override time.
packages/stack-shared/src/utils/arrays.tsx (2)
205-216
: Deduplicate with a generic isArrayOf helper; add a “plain object” variant to avoid ambiguity.
- The three primitives share identical structure; factor via a reusable helper.
- isObjectArray currently treats arrays as “object” (true) and excludes functions; the TS type object includes functions, so the runtime/type semantics can surprise callers. Consider leaving isObjectArray as-is but also exporting isPlainObjectArray that excludes arrays and nulls and narrows to Record<string, unknown>[].
Apply this diff:
+const isArrayOf = <T>(pred: (v: unknown) => v is T) => + (arr: unknown): arr is T[] => + Array.isArray(arr) && arr.every(pred); + -export function isStringArray(arr: unknown): arr is string[] { - return Array.isArray(arr) && arr.every((item) => typeof item === "string"); -} +export const isStringArray = isArrayOf((item): item is string => typeof item === "string"); -export function isNumberArray(arr: unknown): arr is number[] { - return Array.isArray(arr) && arr.every((item) => typeof item === "number"); -} +export const isNumberArray = isArrayOf((item): item is number => typeof item === "number"); -export function isBooleanArray(arr: unknown): arr is boolean[] { - return Array.isArray(arr) && arr.every((item) => typeof item === "boolean"); -} +export const isBooleanArray = isArrayOf((item): item is boolean => typeof item === "boolean"); export function isObjectArray(arr: unknown): arr is object[] { return Array.isArray(arr) && arr.every((item) => typeof item === "object" && item !== null); } + +export function isPlainObjectArray(arr: unknown): arr is Record<string, unknown>[] { + return Array.isArray(arr) + && arr.every((item) => typeof item === "object" && item !== null && !Array.isArray(item)); +}
217-228
: Broaden tests to lock in semantics (functions, array-of-arrays, and plain objects).Add cases to document behavior explicitly and cover the new isPlainObjectArray.
Apply this diff:
import.meta.vitest?.test("is<Type>Array", ({ expect }) => { expect(isStringArray([])).toBe(true); expect(isNumberArray([1, 2, 3])).toBe(true); expect(isBooleanArray([true, false, true])).toBe(true); expect(isObjectArray([{ a: 1 }, { b: 2 }, { c: 3 }])).toBe(true); + expect(isPlainObjectArray([{ a: 1 }, { b: 2 }])).toBe(true); + expect(isObjectArray([[1], [2]])).toBe(true); // arrays are objects + expect(isPlainObjectArray([[1], [2]])).toBe(false); // but not “plain objects” + expect(isObjectArray([() => {}])).toBe(false); // functions are not typeof "object" expect(isStringArray([1, 2, 3])).toBe(false); expect(isNumberArray(["a", "b", "c"])).toBe(false); expect(isBooleanArray([1, 2, 3])).toBe(false); expect(isObjectArray([1, 2, 3])).toBe(false); expect(isObjectArray([{ a: 1 }, null, { b: 2 }])).toBe(false); expect(isObjectArray([{ a: 1 }, undefined, { b: 2 }])).toBe(false); });apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/[workflowId]/page.tsx (3)
4-4
: Use inline Alert for blocking errors and disable Save when unchanged.Per dashboard UI guideline, prefer alerts (not toasts) for blocking errors. Also prevent no-op saves.
- import { Button, Card, CardContent, CardHeader, CardTitle, toast } from "@stackframe/stack-ui"; + import { Button, Card, CardContent, CardHeader, CardTitle, toast, Alert, AlertDescription } from "@stackframe/stack-ui";- const [workflowContent, setWorkflowContent] = useState(""); + const [workflowContent, setWorkflowContent] = useState(""); + const [errorMsg, setErrorMsg] = useState<string | null>(null); + const hasChanges = workflowContent !== (workflow?.tsSource ?? "");const handleSave = async () => { setIsLoading(true); try { + setErrorMsg(null); await project.updateConfig({ [`workflows.availableWorkflows.${workflowId}.tsSource`]: workflowContent }); toast({ title: "Workflow saved successfully" }); } catch (error) { - toast({ title: "Failed to save workflow", variant: "destructive" }); + setErrorMsg("Failed to save workflow. Please try again."); } finally { setIsLoading(false); } };- <Button onClick={handleSave} size="sm" disabled={isLoading}> + <Button onClick={handleSave} size="sm" disabled={isLoading || !hasChanges}> <Save className="h-4 w-4 mr-2" /> {isLoading ? "Saving..." : "Save"} </Button><CardContent> <div className="relative"> + {errorMsg && ( + <Alert variant="destructive" className="mb-3"> + <AlertDescription>{errorMsg}</AlertDescription> + </Alert> + )} <textarea value={workflowContent} onChange={(e) => setWorkflowContent(e.target.value)} className="w-full min-h-[600px] p-4 font-mono text-sm bg-muted/50 border rounded-md resize-none focus:outline-none focus:ring-2 focus:ring-primary" spellCheck={false} placeholder="Enter your workflow code here..." disabled={isLoading} /> </div> </CardContent>Also applies to: 23-25, 32-44, 73-76, 88-99
54-55
: Render the workflow id as code; avoid JSON.stringify in UI text.- <p className="text-muted-foreground mb-4">The workflow {JSON.stringify(workflowId)} was not found.</p> + <p className="text-muted-foreground mb-4">The workflow <code>{workflowId}</code> was not found.</p>
80-80
: Avoid always-on scrollbars; use overflow-auto and Tailwind basis.- <div className="flex gap-6 flex-1" style={{ flexBasis: "0px", overflow: "scroll" }}> + <div className="flex gap-6 flex-1 basis-0 overflow-auto">apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx (2)
9-14
: Export Workflow type for reuse across pages (remove anys elsewhere).-type Workflow = { +export type Workflow = { id: string, displayName: string, tsSource: string, enabled: boolean, };
127-134
: Consider limiting content search for performance on large tsSource.For better UX on big sources: search id/displayName by default and add a “Search code” toggle, or debounce and skip tsSource when query < 3 chars.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx (4)
113-114
: Remove unsupported width prop on Input; use className or style.- onChange={(e) => setDisplayName(e.target.value)} - width="200px" + onChange={(e) => setDisplayName(e.target.value)} + className="w-[200px]"
11-11
: Type workflows explicitly and fix blocking error toast in Edit form.
Reuse Workflow type; use inline Alert for validation instead of toast.-import { WorkflowList } from "./workflow-list"; +import { WorkflowList, type Workflow } from "./workflow-list";- const [showEditDialog, setShowEditDialog] = useState(false); - const [editingWorkflow, setEditingWorkflow] = useState<any>(null); + const [showEditDialog, setShowEditDialog] = useState(false); + const [editingWorkflow, setEditingWorkflow] = useState<Workflow | null>(null);- const handleEditWorkflow = (workflow: any) => { + const handleEditWorkflow = (workflow: Workflow) => {- const handleDuplicateWorkflow = async (workflow: any) => { + const handleDuplicateWorkflow = async (workflow: Workflow) => {- const handleToggleEnabled = async (workflow: any) => { + const handleToggleEnabled = async (workflow: Workflow) => {- workflow: any, + workflow: Workflow,- const [displayName, setDisplayName] = useState(workflow.displayName); - const [tsSource, setTsSource] = useState(workflow.tsSource); - const [isSubmitting, setIsSubmitting] = useState(false); + const [displayName, setDisplayName] = useState<string>(workflow.displayName); + const [tsSource, setTsSource] = useState<string>(workflow.tsSource); + const [isSubmitting, setIsSubmitting] = useState(false); + const [formError, setFormError] = useState<string | null>(null);- if (!displayName) { - toast({ title: "Please fill in all required fields", variant: "destructive" }); - return; - } + if (!displayName) { + setFormError("Please fill in all required fields"); + return; + } + setFormError(null);return ( <> <div className="space-y-4"> + {formError && ( + <Alert variant="destructive"> + <AlertDescription>{formError}</AlertDescription> + </Alert> + )} <div> <Label htmlFor="edit-workflow-id">Workflow ID</Label> <Input id="edit-workflow-id" value={workflow.id} disabled className="bg-muted" /> </div>Also applies to: 136-137, 158-161, 185-189, 207-214, 293-299, 301-306, 315-345
232-232
: Avoid inline overflow:scroll; use Tailwind utilities.- <div className="flex gap-6 flex-1" style={{ flexBasis: "0px", overflow: "scroll" }}> + <div className="flex gap-6 flex-1 basis-0 overflow-auto">
186-195
: Optional: make duplicate IDs robust against races.
If two duplicates happen quickly, the while-loop on a stale snapshot can collide. Consider using generateUuid() for the new id or delegating uniqueness to the backend.packages/stack-shared/src/utils/numbers.tsx (3)
15-25
: Avoid string math in the loop; keep it numeric until the final format.Reduces conversions and clarifies intent. Also adds a graceful clamp when exceeding the last suffix.
- let current = toFixedMax(num, 1); - let lastSuffix = ""; - for (const [difference, suffix] of magnitudes) { - if (+current >= difference) { - current = toFixedMax(+current / difference, 1); - lastSuffix = suffix; - } else { - break; - } - } - return current + lastSuffix; + let value = num; + let lastSuffix = ""; + for (const [, suffix] of magnitudes) { + if (value >= 1_000) { + value = value / 1_000; + lastSuffix = suffix; + } else { + break; + } + } + // Optional: clamp when we run out of suffixes (prevents "1000T"/"1000Q") + const maxSuffix = magnitudes[magnitudes.length - 1][1]; + if (lastSuffix === maxSuffix && value >= 1_000) { + return toFixedMax(value, 1) + lastSuffix + "+"; + } + return toFixedMax(value, 1) + lastSuffix;
1-7
: Nit: This file has no JSX; consider .ts instead of .tsx.Minor build/IDE signal improvement.
38-43
: Adjust magnitude suffixes and test expectations for short-scale abbreviations
Mapping entries ("bn"
,"bln"
,"trln"
) and the corresponding tests in packages/stack-shared/src/utils/numbers.tsx must be updated to use“B”
,“T”
,“Q”
when switching to short-scale symbols.packages/stack-shared/src/utils/promises.tsx (1)
416-418
: Update timeoutThrow to match the new timeout signatureThe
timeoutThrow
function should also accept the new union type to maintain API consistency withtimeout
.-export async function timeoutThrow<T>(promise: Promise<T>, ms: number): Promise<T> { +export async function timeoutThrow<T>(promiseOrFunc: Promise<T> | (() => Promise<T>), ms: number): Promise<T> { - return Result.orThrow(await timeout(promise, ms)); + return Result.orThrow(await timeout(promiseOrFunc, ms)); }apps/backend/prisma/schema.prisma (2)
776-787
: Consider stronger typing and indexing for DataVaultEntry.
- If
hashedKey
has a fixed/hash length, constrain it (e.g.,@db.VarChar(64)
).encrypted
appears to carry binary data encoded as JSON strings. Prefer a structured JSON with explicit fields and length checks, or split intoBytes
fields to avoid double-encoding overhead.- If lookups happen by
hashedKey
alone, add an index on[tenancyId, hashedKey]
to avoid relying solely on the composite unique.Example diff:
model DataVaultEntry { - hashedKey String - encrypted Json // Contains { edkBase64, ciphertextBase64 } from encryptWithKms() + hashedKey String @db.VarChar(64) + // Contains { edkBase64, ciphertextBase64 } from encryptWithKms() + encrypted Json createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@id([tenancyId, id]) @@unique([tenancyId, storeId, hashedKey]) @@index([tenancyId, storeId]) + @@index([tenancyId, hashedKey]) }
824-835
: Double-check concurrency semantics for CurrentlyCompilingWorkflow.Primary key
[tenancyId, workflowId, sourceHash]
permits multiple “compiling” rows per workflow across source revisions. If only one compile may run per workflow/source, this is fine; otherwise, consider a uniqueness guard on(tenancyId, workflowId)
for active compiles (enforced via status column or separate “active” table). Also consider a TTL/cleanup job for stale rows.apps/backend/src/lib/workflows.tsx (5)
61-74
: Guard against unknown triggers at runtime.If
registeredTriggers.get(triggerData.type)
is undefined, calling it will throw a vague TypeError. Fail fast with a clear error.Apply:
- const trigger = registeredTriggers.get(triggerData.type); - return { - triggerOutput: trigger(triggerData.data), - }; + const trigger = registeredTriggers.get(triggerData.type); + if (!trigger) { + throw new Error(\`Unknown trigger: \${triggerData.type}\`); + } + return { triggerOutput: trigger(triggerData.data) };
85-123
: Timeout handling is good; also consider honoring workflow.enabled.You check existence but not
enabled
. If disabled workflows should not compile, gate here.Example:
- const workflow = tenancy.config.workflows.availableWorkflows[workflowId]; + const workflow = tenancy.config.workflows.availableWorkflows[workflowId]; + if (workflow.enabled === false) { + return Result.error({ compileError: `Workflow ${workflowId} is disabled` }); + }
178-184
: Unimplemented getCompiledWorkflows blocks triggerWorkflows.This currently throws. Add a minimal DB-backed implementation with a serializable transaction to avoid races: compute source hashes, upsert “compiling” rows, perform compile with timeout, write results to
CompiledWorkflow
, then return a Map of latest compiled artifacts.I can draft an implementation sketch with Prisma transaction boundaries if you’d like.
191-197
: Replace placeholder secrets with tenancy-aware values before shipping.Hardcoded placeholders for
STACK_PUBLISHABLE_CLIENT_KEY
andSTACK_SECRET_SERVER_KEY
will break runtime behavior.Would you like me to thread these from
tenancy.config
and add validation that they’re present for non-test environments?
231-237
: Surface per-workflow results and handle partial failures.
Promise.all
will reject on the first failure and mask others. ConsiderPromise.allSettled
and returning a map of successes/failures for observability.Apply:
export async function triggerWorkflows(tenancy: Tenancy, trigger: WorkflowTrigger) { const compiledWorkflows = await getCompiledWorkflows(tenancy); - const promises = [...compiledWorkflows].map(async ([workflowId, compiledWorkflow]) => { - await triggerWorkflow(tenancy, compiledWorkflow, trigger); - }); - await Promise.all(promises); + const promises = [...compiledWorkflows].map(([_, cw]) => triggerWorkflow(tenancy, cw, trigger)); + await Promise.allSettled(promises); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (13)
apps/backend/prisma/schema.prisma
(2 hunks)apps/backend/src/lib/email-rendering.tsx
(1 hunks)apps/backend/src/lib/freestyle.tsx
(2 hunks)apps/backend/src/lib/workflows.tsx
(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx
(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/[workflowId]/page.tsx
(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx
(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx
(1 hunks)packages/stack-shared/src/config/schema.ts
(5 hunks)packages/stack-shared/src/utils/arrays.tsx
(1 hunks)packages/stack-shared/src/utils/numbers.tsx
(2 hunks)packages/stack-shared/src/utils/promises.tsx
(1 hunks)packages/template/src/lib/stack-app/apps/implementations/common.ts
(2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
{apps/dashboard,apps/dev-launchpad,packages/stack-ui,packages/react}/**/*.{tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
For blocking alerts and errors in UI, do not use toast notifications; use alerts instead
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/[workflowId]/page.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Prefer ES6 Map over Record when representing key–value collections
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/[workflowId]/page.tsx
packages/stack-shared/src/utils/arrays.tsx
packages/stack-shared/src/utils/promises.tsx
packages/template/src/lib/stack-app/apps/implementations/common.ts
apps/backend/src/lib/workflows.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx
apps/backend/src/lib/email-rendering.tsx
apps/backend/src/lib/freestyle.tsx
packages/stack-shared/src/config/schema.ts
packages/stack-shared/src/utils/numbers.tsx
{apps/dashboard,apps/dev-launchpad,packages/stack-ui,packages/react}/**/*.{tsx,jsx,css}
📄 CodeRabbit inference engine (AGENTS.md)
Keep hover/click animations snappy; avoid pre-transition delays on hover and apply transitions after the action (e.g., fade-out on hover end)
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/[workflowId]/page.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx
packages/template/**
📄 CodeRabbit inference engine (AGENTS.md)
When modifying the SDK copies, make changes in packages/template (source of truth)
Files:
packages/template/src/lib/stack-app/apps/implementations/common.ts
🧬 Code graph analysis (10)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/page-layout.tsx (1)
PageLayout
(3-42)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx (5)
apps/dashboard/src/components/illustrated-info.tsx (1)
IllustratedInfo
(4-31)packages/stack-shared/src/utils/uuids.tsx (1)
generateUuid
(3-8)packages/stack-shared/src/utils/strings.tsx (1)
deindent
(235-238)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx (1)
useAdminApp
(27-34)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx (1)
WorkflowList
(108-160)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/[workflowId]/page.tsx (2)
apps/dashboard/src/components/router.tsx (1)
useRouter
(15-33)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx (1)
useAdminApp
(27-34)
packages/stack-shared/src/utils/promises.tsx (1)
packages/stack-shared/src/utils/results.tsx (2)
Result
(4-12)Result
(26-56)
packages/template/src/lib/stack-app/apps/implementations/common.ts (1)
packages/stack-shared/src/utils/errors.tsx (1)
throwErr
(10-19)
apps/backend/src/lib/workflows.tsx (10)
packages/stack-shared/src/utils/bytes.tsx (1)
encodeBase64
(114-116)packages/stack-shared/src/utils/crypto.tsx (1)
hash
(111-113)packages/stack-shared/src/utils/esbuild.tsx (1)
bundleJavaScript
(33-128)apps/backend/src/lib/tenancies.tsx (1)
Tenancy
(47-47)packages/stack-shared/src/utils/errors.tsx (3)
StackAssertionError
(69-85)captureError
(126-134)errorToNiceString
(89-92)packages/stack-shared/src/utils/promises.tsx (1)
timeout
(390-396)packages/stack-shared/src/utils/arrays.tsx (1)
isStringArray
(205-207)packages/stack-shared/src/utils/uuids.tsx (1)
generateUuid
(3-8)apps/backend/src/lib/freestyle.tsx (1)
Freestyle
(7-45)apps/backend/src/prisma-client.tsx (1)
getPrismaClientForTenancy
(51-53)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx (2)
apps/dashboard/src/components/router.tsx (1)
useRouter
(15-33)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/list-section.tsx (1)
ListSection
(19-90)
apps/backend/src/lib/email-rendering.tsx (1)
apps/backend/src/lib/freestyle.tsx (1)
Freestyle
(7-45)
apps/backend/src/lib/freestyle.tsx (2)
packages/stack-shared/src/utils/env.tsx (2)
getEnvVariable
(16-58)getNodeEnvironment
(65-67)packages/stack-shared/src/utils/errors.tsx (1)
StackAssertionError
(69-85)
packages/stack-shared/src/config/schema.ts (2)
packages/stack-shared/src/schema-fields.ts (5)
userSpecifiedIdSchema
(426-426)yupObject
(247-251)yupRecord
(283-322)yupString
(187-190)yupBoolean
(195-198)packages/stack-shared/src/utils/errors.tsx (1)
StackAssertionError
(69-85)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
- GitHub Check: build (22.x)
- GitHub Check: lint_and_build (latest)
- GitHub Check: docker
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test
- GitHub Check: setup-tests
- GitHub Check: docker
- GitHub Check: all-good
- GitHub Check: Security Check
🔇 Additional comments (21)
packages/stack-shared/src/config/schema.ts (5)
189-190
: Wiring workflows into branchConfigSchema — LGTMSchema composition looks consistent with other branch-level sections.
508-514
: Organization defaults for workflows — LGTMDefaults align with existing pattern (emails/themes/templates) and should render sensible fallback UI.
878-880
: Droppingschema
from error.extraData is safe
Search for.extraData.schema
across the repo returned no consumer references—telemetry/debug code does not inspect it.
45-47
: Drop redundant.defined()
on connectionStringId schema
The.defined()
call is unnecessary and inconsistent; please verify no existing configs use invalid connectionString IDs (e.g., starting with a digit or containing disallowed characters).- userSpecifiedIdSchema("connectionStringId").defined(), + userSpecifiedIdSchema("connectionStringId"),
181-187
: Verify compatibility of existing data vault store IDs
The switch to userSpecifiedIdSchema("storeId") enforces stricter ID rules (no leading digits or illegal characters). Automated scanning didn’t flag any store keys in code—please manually confirm your database/config entries comply or add a migration.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx (1)
43-45
: Nice snappy hover behavior.
Switching to duration-0 on hover matches the “snappy interactions” guideline.packages/stack-shared/src/utils/numbers.tsx (1)
32-35
: LGTM: Boundary rounding around 1M is correct.The transitions at 999_499, 999_500, 999_949, 999_950 behave as expected with 1-decimal rounding.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx (1)
776-776
: Title change looks goodThe title change from "Payments" to "Offers & Items" better reflects the page content and terminology.
packages/template/src/lib/stack-app/apps/implementations/common.ts (3)
68-68
: LGTM! Good addition of environment variable fallbacksThe addition of fallback to
STACK_PROJECT_ID
whenNEXT_PUBLIC_STACK_PROJECT_ID
is not set improves flexibility for server-side configurations.
72-72
: LGTM! Consistent fallback patternThe addition of fallback to
STACK_PUBLISHABLE_CLIENT_KEY
maintains consistency with the project ID fallback pattern.
84-84
: LGTM! Complete fallback implementationThe addition of fallback to
STACK_EXTRA_REQUEST_HEADERS
completes the consistent fallback pattern across all three environment variable getters.packages/stack-shared/src/utils/promises.tsx (1)
390-391
: Good enhancement for lazy promise creationThe updated signature accepting
Promise<T> | (() => Promise<T>)
enables lazy promise creation, which is particularly useful for avoiding side effects until the timeout actually starts. This aligns well with the workflow orchestration needs where expensive operations should only be initiated when needed.apps/backend/src/lib/email-rendering.tsx (2)
115-115
: LGTM! Simplified Freestyle instantiationThe removal of explicit API key handling simplifies the code since Freestyle now handles the environment variable fallback internally.
122-125
: Improved error handling with Result patternThe updated error handling properly checks for
output.status === "error"
and returns the error directly, aligning with the Result pattern used throughout the codebase. The data access pathoutput.data.result
correctly reflects the new Result wrapper structure.apps/backend/src/lib/freestyle.tsx (3)
10-11
: Good API design with optional apiKeyMaking the
apiKey
optional with a default empty object improves flexibility. The fallback togetEnvVariable("STACK_FREESTYLE_API_KEY")
maintains backward compatibility while allowing explicit key override.
35-41
: Robust error unwrapping with Result patternThe error unwrapping logic thoroughly handles nested error structures that may come from the Freestyle service. The multi-level unwrapping (wrap1 through wrap4) with JSON parsing ensures that the actual error is extracted regardless of how it's wrapped. This is a good defensive programming practice.
13-17
: Ensure test environments and CI don’t default to the mock key
- apps/backend/.env.development sets STACK_FREESTYLE_API_KEY=mock_stack_freestyle_key (line 48) – add or update a .env.test (or test config) to use a real API key instead of falling back to the mock.
- Verify CI workflows (e.g. under .github/workflows/) explicitly set STACK_FREESTYLE_API_KEY to a non-mock value.
apps/backend/prisma/schema.prisma (3)
27-27
: No-op change acknowledged.Whitespace-only; no behavioral impact.
837-857
: Enforce XOR for compiledCode vs compileError and add “latest” retrieval indexes.
- Add a CHECK constraint to ensure exactly one of
compiledCode
orcompileError
is set.- Add an index to fetch the latest compiled artifact per workflow quickly (by
compilationVersion
orcompiledAt
).
[Suggest_essential_refactor]
Suggested changes:model CompiledWorkflow { tenancyId String @db.Uuid id String @default(uuid()) @db.Uuid workflowId String @db.Uuid // note: The workflow with this ID may have been edited or deleted in the meantime, so there may be multiple CompiledWorkflows with the same workflowId compilationVersion Int sourceHash String // exactly one of [compiledCode, compileError] must be set compiledCode String? compileError String? compiledAt DateTime @default(now()) registeredTriggers String[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt WorkflowExecution WorkflowExecution[] @@id([tenancyId, id]) @@unique([tenancyId, workflowId, compilationVersion, sourceHash]) + @@index([tenancyId, workflowId, compilationVersion(sort: Desc)], name: "CompiledWorkflow_latest_by_version") + @@index([tenancyId, workflowId, compiledAt(sort: Desc)], name: "CompiledWorkflow_latest_by_time") }And in SQL migration:
ALTER TABLE "CompiledWorkflow" ADD CONSTRAINT "compiledworkflow_code_xor_error" CHECK ( (("compiledCode" IS NULL) <> ("compileError" IS NULL)) );
808-822
: Add cascade delete, recency index, and document triggerIds invariant
- compiledWorkflow relation: add
onDelete: Cascade
(verify your Prisma version supports referential actions or enablereferentialActions
inpreviewFeatures
)- Add
@@index([tenancyId, compiledWorkflowId, createdAt(sort: Desc)], name: "WorkflowExecution_byCompiled_recent")
- Add a comment above
triggerIds
explaining its consistency invariant with thetriggers
relationapps/backend/src/lib/workflows.tsx (1)
125-176
: Tests confirm behavior; they also expose the missing "compile" union type.Once the union is fixed, these expectations align. No further action here.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx
Show resolved
Hide resolved
[1_000, "M"], | ||
[1_000, "bn"], | ||
[1_000, "bln"], | ||
[1_000, "trln"], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
❓ Verification inconclusive
Fix incorrect/ambiguous suffix mapping beyond 1e9 (bn/bln/trln).
Current mapping labels 10^12 as "bln" and 10^15 as "trln", which conflicts with the short scale (US) and mixes styles ("bn" vs "bln"). This is user-facing and misleading.
Apply short-scale, widely-recognized symbols:
[1_000, "k"],
- [1_000, "M"],
- [1_000, "bn"],
- [1_000, "bln"],
- [1_000, "trln"],
+ [1_000, "M"], // 10^6
+ [1_000, "B"], // 10^9 (billion)
+ [1_000, "T"], // 10^12 (trillion)
+ [1_000, "Q"], // 10^15 (quadrillion)
If you must keep word-like abbreviations, use a consistent short scale: ["k","M","bn","trln","qdln"].
To align expectations, update the tests accordingly (see below).
I can push the test updates once you confirm the preferred style.
Use consistent short-scale suffixes for numeric formatting
Replace ambiguous word-like suffixes beyond million with standard short-scale symbols and update tests accordingly:
- [1_000_000, "M"], // 10^6
- [1_000_000_000, "bn"], // 10^9
- [1_000_000_000_000, "bln"], // 10^12
- [1_000_000_000_000_000, "trln"], // 10^15
+ [1_000_000, "M"], // 10^6
+ [1_000_000_000, "B"], // 10^9
+ [1_000_000_000_000, "T"], // 10^12
+ [1_000_000_000_000_000, "Q"], // 10^15
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
[1_000, "M"], | |
[1_000, "bn"], | |
[1_000, "bln"], | |
[1_000, "trln"], | |
[1_000_000, "M"], // 10^6 | |
[1_000_000_000, "B"], // 10^9 | |
[1_000_000_000_000, "T"], // 10^12 | |
[1_000_000_000_000_000, "Q"], // 10^15 |
🤖 Prompt for AI Agents
In packages/stack-shared/src/utils/numbers.tsx around lines 3 to 6, the numeric
suffixes use ambiguous word-like forms ("bn", "bln", "trln") instead of standard
short-scale symbols; update the mappings to use consistent short-scale suffixes
(e.g., "K" for thousand, "M" for million, "B" for billion, "T" for trillion,
etc.) and adjust any downstream logic that formats numbers accordingly, then
update unit tests to expect the new symbols.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (3)
apps/backend/src/lib/workflows.tsx (3)
16-16
: Include "compile" in the registered trigger type union.
compileWorkflowSource
registers a "compile" trigger and tests assert it; the union excludes it, forcing unsafe casts.Apply:
-type WorkflowRegisteredTriggerType = "sign-up"; +type WorkflowRegisteredTriggerType = "compile" | "sign-up";
318-332
: Return execution metadata from triggerWorkflowRaw.You generate
triggerId
andexecutionId
but discard them. Persisting triggers/executions later needs these IDs to correlate callback chains; return them alongside the result.Apply:
-async function triggerWorkflowRaw(tenancy: Tenancy, compiledWorkflowCode: string, trigger: WorkflowTrigger): Promise<Result<unknown, string>> { +async function triggerWorkflowRaw( + tenancy: Tenancy, + compiledWorkflowCode: string, + trigger: WorkflowTrigger +): Promise<Result<{ result: unknown, triggerId: string, executionId: string }, string>> { const triggerId = generateUuid(); const executionId = trigger.type === "callback" ? trigger.executionId : generateUuid(); const freestyle = new Freestyle(); const freestyleRes = await freestyle.executeScript(compiledWorkflowCode, { envVars: { STACK_WORKFLOW_TRIGGER_DATA: JSON.stringify(trigger), STACK_PROJECT_ID: tenancy.project.id, STACK_PUBLISHABLE_CLIENT_KEY: "insert actual publishable client key here", STACK_SECRET_SERVER_KEY: "insert actual secret server key here", }, }); - return Result.map(freestyleRes, (data) => data.result); + return Result.map(freestyleRes, (data) => ({ + result: data.result, + triggerId, + executionId, + })); }
334-361
: Fix incorrect database associations and use generated IDs for proper execution tracking.The current implementation has critical issues:
- Uses
compiledWorkflow.id
(the compiled workflow ID) as the execution ID in thewhere
clause, which is incorrect- Doesn't preserve the
triggerId
andexecutionId
generated intriggerWorkflowRaw
- Always tries to
connectOrCreate
with incorrect IDs instead of creating new executionsApply this fix to properly track executions and triggers:
async function triggerWorkflow(tenancy: Tenancy, compiledWorkflow: CompiledWorkflow, trigger: WorkflowTrigger): Promise<Result<void, string>> { if (compiledWorkflow.compiledCode === null) { return Result.error(`Workflow ${compiledWorkflow.id} failed to compile: ${compiledWorkflow.compileError}`); } const res = await triggerWorkflowRaw(tenancy, compiledWorkflow.compiledCode, trigger); const prisma = await getPrismaClientForTenancy(tenancy); - await prisma.workflowTrigger.create({ - data: { - triggerData: trigger, - ...(res.status === "ok" ? { output: (res.data ?? Prisma.JsonNull) as any } : { error: res.error }), - execution: { - connectOrCreate: { - where: { - tenancyId_id: { - tenancyId: tenancy.id, - id: compiledWorkflow.id, - }, - }, - create: { - tenancyId: tenancy.id, - compiledWorkflowId: compiledWorkflow.id, - }, - }, - }, - }, - }); + if (res.status === "ok") { + await prisma.workflowTrigger.create({ + data: { + tenancyId: tenancy.id, + id: res.data.triggerId, + triggerData: trigger, + output: (res.data.result ?? Prisma.JsonNull) as any, + execution: { + create: { + tenancyId: tenancy.id, + id: res.data.executionId, + compiledWorkflowId: compiledWorkflow.id, + }, + }, + }, + }); + } else { + // Generate IDs for failed executions too, for tracking + await prisma.workflowTrigger.create({ + data: { + tenancyId: tenancy.id, + id: generateUuid(), + triggerData: trigger, + error: res.error, + execution: { + create: { + tenancyId: tenancy.id, + id: generateUuid(), + compiledWorkflowId: compiledWorkflow.id, + }, + }, + }, + }); + } return Result.ok(undefined); }
🧹 Nitpick comments (1)
apps/backend/src/lib/workflows.tsx (1)
287-287
: Remove console.log statement.Debug logging should use proper logging infrastructure instead of console.log.
- console.log(`Compiled workflow ${workflowId}`); + // Use proper logging infrastructure if needed + // logger.info(`Compiled workflow ${workflowId}`);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (6)
apps/backend/prisma/migrations/20250902221648_workflows/migration.sql
(1 hunks)apps/backend/prisma/schema.prisma
(2 hunks)apps/backend/src/lib/workflows.tsx
(1 hunks)apps/backend/src/prisma-client.tsx
(2 hunks)apps/backend/src/utils/vercel.tsx
(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx
(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/backend/prisma/schema.prisma
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Prefer ES6 Map over Record when representing key–value collections
Files:
apps/backend/src/utils/vercel.tsx
apps/backend/src/prisma-client.tsx
apps/backend/src/lib/workflows.tsx
🧬 Code graph analysis (1)
apps/backend/src/lib/workflows.tsx (11)
packages/stack-shared/src/utils/bytes.tsx (1)
encodeBase64
(114-116)packages/stack-shared/src/utils/crypto.tsx (1)
hash
(111-113)packages/stack-shared/src/utils/esbuild.tsx (1)
bundleJavaScript
(33-128)apps/backend/src/lib/tenancies.tsx (1)
Tenancy
(47-47)packages/stack-shared/src/utils/errors.tsx (4)
StackAssertionError
(69-85)captureError
(126-134)errorToNiceString
(89-92)throwErr
(10-19)packages/stack-shared/src/utils/promises.tsx (2)
timeout
(390-396)wait
(260-268)packages/stack-shared/src/utils/arrays.tsx (1)
isStringArray
(205-207)apps/backend/src/prisma-client.tsx (3)
globalPrismaClient
(31-31)retryTransaction
(124-193)getPrismaClientForTenancy
(51-53)apps/backend/src/utils/vercel.tsx (1)
allPromisesAndWaitUntilEach
(10-15)packages/stack-shared/src/utils/uuids.tsx (1)
generateUuid
(3-8)apps/backend/src/lib/freestyle.tsx (1)
Freestyle
(7-45)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
- GitHub Check: build (22.x)
- GitHub Check: lint_and_build (latest)
- GitHub Check: build (22.x)
- GitHub Check: docker
- GitHub Check: docker
- GitHub Check: restart-dev-and-test
- GitHub Check: setup-tests
- GitHub Check: all-good
- GitHub Check: Security Check
🔇 Additional comments (4)
apps/backend/src/utils/vercel.tsx (1)
10-15
: LGTM!The implementation of
allPromisesAndWaitUntilEach
correctly registers each promise with Vercel's background execution and then awaits all results viaPromise.all
. The function properly propagates errors and maintains the original promise results ordering.apps/backend/prisma/migrations/20250902221648_workflows/migration.sql (2)
37-37
: Fix typo in primary key constraint.There's a typo in the primary key constraint name: "tenantryId" should be "tenancyId" to match the column name.
- CONSTRAINT "CurrentlyCompilingWorkflow_pkey" PRIMARY KEY ("tenantryId","workflowId","compilationVersion","sourceHash") + CONSTRAINT "CurrentlyCompilingWorkflow_pkey" PRIMARY KEY ("tenancyId","workflowId","compilationVersion","sourceHash")Likely an incorrect or invalid review comment.
58-58
: Index name exceeds PostgreSQL's 63-character limit.The index name "CompiledWorkflow_tenancyId_workflowId_compilationVersion_so_key" is 64 characters long, which exceeds PostgreSQL's maximum identifier length of 63 characters. This will cause the index name to be truncated, potentially leading to naming conflicts or confusion.
Consider shortening the index name to stay within the 63-character limit:
-CREATE UNIQUE INDEX "CompiledWorkflow_tenancyId_workflowId_compilationVersion_so_key" ON "CompiledWorkflow"("tenancyId", "workflowId", "compilationVersion", "sourceHash"); +CREATE UNIQUE INDEX "CompiledWorkflow_tenancy_workflow_version_hash_key" ON "CompiledWorkflow"("tenancyId", "workflowId", "compilationVersion", "sourceHash");Likely an incorrect or invalid review comment.
apps/backend/src/prisma-client.tsx (1)
124-126
: LGTM! Clean API extension for transaction isolation levels.The addition of the optional
options
parameter with isolation level control is well-designed. The default behavior is preserved, and the API provides a clean way to request serializable isolation when needed for workflow compilation and other critical operations.
apps/backend/src/lib/workflows.tsx
Outdated
}; | ||
|
||
let retryInfo = []; | ||
const prisma = globalPrismaClient; //await getPrismaClientForTenancy(tenancy); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use tenancy-specific Prisma client instead of global client.
The code uses globalPrismaClient
but has the correct getPrismaClientForTenancy(tenancy)
commented out. This could cause issues in multi-tenant environments where workflows should be isolated per tenancy.
- const prisma = globalPrismaClient; //await getPrismaClientForTenancy(tenancy);
+ const prisma = await getPrismaClientForTenancy(tenancy);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const prisma = globalPrismaClient; //await getPrismaClientForTenancy(tenancy); | |
// Replace global client with tenancy-scoped Prisma client | |
const prisma = await getPrismaClientForTenancy(tenancy); |
🤖 Prompt for AI Agents
In apps/backend/src/lib/workflows.tsx around line 252, the file currently
assigns prisma = globalPrismaClient but should use a tenancy-scoped client;
replace that line with an awaited call to getPrismaClientForTenancy(tenancy)
(e.g. const prisma = await getPrismaClientForTenancy(tenancy)), ensure the
surrounding function is async or handle the promise, remove reliance on
globalPrismaClient, and add minimal error handling if the tenancy client is not
returned.
apps/backend/src/lib/workflows.tsx
Outdated
const promises = [...compiledWorkflows].map(async ([workflowId, compiledWorkflow]) => { | ||
await triggerWorkflow(tenancy, compiledWorkflow, trigger); | ||
}); | ||
await Promise.all(promises); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add error handling for workflow trigger failures.
The function doesn't handle or log errors from individual workflow triggers, which could make debugging difficult when workflows fail.
export async function triggerWorkflows(tenancy: Tenancy, trigger: WorkflowTrigger) {
const compiledWorkflows = await compileAndGetEnabledWorkflows(tenancy);
const promises = [...compiledWorkflows].map(async ([workflowId, compiledWorkflow]) => {
- await triggerWorkflow(tenancy, compiledWorkflow, trigger);
+ try {
+ const result = await triggerWorkflow(tenancy, compiledWorkflow, trigger);
+ if (result.status === "error") {
+ captureError("workflow-trigger-failed", new StackAssertionError(`Failed to trigger workflow ${workflowId}`, {
+ workflowId,
+ error: result.error,
+ trigger
+ }));
+ }
+ } catch (e) {
+ captureError("workflow-trigger-exception", e);
+ }
});
await Promise.all(promises);
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const promises = [...compiledWorkflows].map(async ([workflowId, compiledWorkflow]) => { | |
await triggerWorkflow(tenancy, compiledWorkflow, trigger); | |
}); | |
await Promise.all(promises); | |
export async function triggerWorkflows(tenancy: Tenancy, trigger: WorkflowTrigger) { | |
const compiledWorkflows = await compileAndGetEnabledWorkflows(tenancy); | |
const promises = [...compiledWorkflows].map(async ([workflowId, compiledWorkflow]) => { | |
try { | |
const result = await triggerWorkflow(tenancy, compiledWorkflow, trigger); | |
if (result.status === "error") { | |
captureError( | |
"workflow-trigger-failed", | |
new StackAssertionError( | |
`Failed to trigger workflow ${workflowId}`, | |
{ | |
workflowId, | |
error: result.error, | |
trigger, | |
} | |
) | |
); | |
} | |
} catch (e) { | |
captureError("workflow-trigger-exception", e); | |
} | |
}); | |
await Promise.all(promises); | |
} |
🤖 Prompt for AI Agents
In apps/backend/src/lib/workflows.tsx around lines 365 to 368, the current
mapping of compiledWorkflows calls triggerWorkflow without handling per-workflow
failures; wrap the async operation for each workflow in a try/catch (or switch
to Promise.allSettled) so individual trigger errors are caught and logged with
the workflowId and error details, ensuring failures are recorded but other
workflows continue to run and Promise.all (or post-processing of allSettled
results) completes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
apps/backend/src/app/api/latest/users/crud.tsx
(3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
apps/backend/src/app/api/latest/**
📄 CodeRabbit inference engine (AGENTS.md)
apps/backend/src/app/api/latest/**
: Organize backend API routes by resource under /api/latest (e.g., auth at /api/latest/auth/, users at /api/latest/users/, teams at /api/latest/teams/, oauth providers at /api/latest/oauth-providers/)
Use the custom route handler system in the backend to ensure consistent API responses
Files:
apps/backend/src/app/api/latest/users/crud.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Prefer ES6 Map over Record when representing key–value collections
Files:
apps/backend/src/app/api/latest/users/crud.tsx
🧬 Code graph analysis (1)
apps/backend/src/app/api/latest/users/crud.tsx (1)
apps/backend/src/lib/workflows.tsx (1)
triggerWorkflows
(363-369)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
- GitHub Check: all-good
- GitHub Check: restart-dev-and-test
- GitHub Check: docker
- GitHub Check: build (22.x)
- GitHub Check: lint_and_build (latest)
- GitHub Check: docker
- GitHub Check: build (22.x)
- GitHub Check: setup-tests
- GitHub Check: Security Check
🔇 Additional comments (1)
apps/backend/src/app/api/latest/users/crud.tsx (1)
8-8
: LGTM on import.Import is correct and used below.
// if the user is not an anonymous user, trigger onSignUp workflows | ||
if (!result.is_anonymous) { | ||
await triggerWorkflows(auth.tenancy, { | ||
type: "sign-up", | ||
userId: result.id, | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Don’t block user creation on workflow execution; make it fire-and-forget.
If any workflow throws, the endpoint 500s even though the user is already persisted. This couples signup UX to arbitrary workflow code and increases latency. Mirror the webhook pattern and dispatch asynchronously.
Apply this diff:
- if (!result.is_anonymous) {
- await triggerWorkflows(auth.tenancy, {
- type: "sign-up",
- userId: result.id,
- });
- }
+ if (!result.is_anonymous) {
+ // Run in the background to avoid failing user creation on workflow errors
+ runAsynchronouslyAndWaitUntil(
+ triggerWorkflows(auth.tenancy, {
+ type: "sign-up",
+ userId: result.id,
+ })
+ );
+ }
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/backend/src/app/api/latest/users/crud.tsx around lines 652 to 658, the
code awaits triggerWorkflows during signup which can cause the endpoint to 500
or increase latency when workflows fail; change this to fire-and-forget by
invoking triggerWorkflows without awaiting and attach a .catch that logs any
errors (or schedule via void Promise.resolve().then(() =>
triggerWorkflows(...)).catch(...)) so failures don’t propagate to the response
path and signup remains non-blocking.
// if we went from anonymous to non-anonymous: | ||
if (oldUser.isAnonymous && data.is_anonymous === false) { | ||
// trigger onSignUp workflows | ||
await triggerWorkflows(auth.tenancy, { | ||
type: "sign-up", | ||
userId: params.user_id, | ||
}); | ||
|
||
// rename the personal team |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Critical: Triggering workflows inside retryTransaction causes duplicate side effects and long transactions.
retryTransaction(...)
may re-run the callback on serialization conflicts. Calling await triggerWorkflows(...)
inside it can double-trigger workflows and holds the DB txn open for external work. Move the trigger outside the transaction and flag within.
Apply this diff:
const prisma = await getPrismaClientForTenancy(auth.tenancy);
- const { user } = await retryTransaction(prisma, async (tx) => {
+ let shouldTriggerSignUp = false;
+ const { user } = await retryTransaction(prisma, async (tx) => {
@@
- if (oldUser.isAnonymous && data.is_anonymous === false) {
- // trigger onSignUp workflows
- await triggerWorkflows(auth.tenancy, {
- type: "sign-up",
- userId: params.user_id,
- });
-
- // rename the personal team
+ if (oldUser.isAnonymous && data.is_anonymous === false) {
+ // mark for post-commit workflow trigger
+ shouldTriggerSignUp = true;
+ // rename the personal team
await tx.team.updateMany({
where: {
tenancyId: auth.tenancy.id,
@@
return {
user,
};
});
+ if (shouldTriggerSignUp) {
+ // Dispatch asynchronously to avoid blocking the update path and duplicate side effects on retries
+ runAsynchronouslyAndWaitUntil(
+ triggerWorkflows(auth.tenancy, {
+ type: "sign-up",
+ userId: params.user_id,
+ })
+ );
+ }
Run to confirm the trigger type is valid and to surface other call sites:
🏁 Script executed:
#!/bin/bash
# Verify 'sign-up' is an allowed WorkflowTrigger type and find all usages
rg -nP --type=ts '\bWorkflowTrigger\b' -C2
rg -nP --type=ts "(\"|')sign-up(\"|')" -C2
Length of output: 8641
🏁 Script executed:
#!/bin/bash
# Show all retryTransaction usages in the users CRUD file with context
rg -nP --type=ts 'retryTransaction\(' -C4 apps/backend/src/app/api/latest/users/crud.tsx
Length of output: 1414
🏁 Script executed:
#!/bin/bash
# Locate definition of retryTransaction
rg -nP --type=ts "async function retryTransaction" -C3 .
rg -nP --type=ts "retryTransaction\\s*=" -C3 .
Length of output: 732
🏁 Script executed:
#!/bin/bash
sed -n '115,180p' apps/backend/src/prisma-client.tsx
Length of output: 3778
🏁 Script executed:
#!/bin/bash
# Extract create path around retryTransaction and subsequent triggerWorkflows
sed -n '630,700p' apps/backend/src/app/api/latest/users/crud.tsx
Length of output: 2300
Move triggerWorkflows call out of retryTransaction in onUpdate
The onUpdate handler invokes await triggerWorkflows(...)
inside retryTransaction
, which can double-trigger workflows on retries and blocks the transaction on external work. Mirror the onCreate pattern: set a flag inside the transaction, then after retryTransaction
completes call
runAsynchronouslyAndWaitUntil(
triggerWorkflows(auth.tenancy, { type: "sign-up", userId: params.user_id })
);
🤖 Prompt for AI Agents
In apps/backend/src/app/api/latest/users/crud.tsx around lines 960 to 968, the
code currently awaits triggerWorkflows(...) inside retryTransaction which can
cause double-triggering on retries and blocks the transaction; change this by
setting a local boolean flag inside the transaction when oldUser.isAnonymous &&
data.is_anonymous === false (do NOT call triggerWorkflows there), then after
retryTransaction completes check that flag and call
runAsynchronouslyAndWaitUntil(triggerWorkflows(auth.tenancy, { type: "sign-up",
userId: params.user_id })); remove the await triggerWorkflows from inside the
transaction so external work runs only once after the transaction succeeds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/stack-shared/src/interface/server-interface.ts (1)
814-834
: AlignsendEmail
to returnvoid
and remove unusedres
Remove the unused
res
variable and let errors propagate by updating the implementation and signature:- const res = await this.sendServerRequest( + await this.sendServerRequest( "/emails/send-email", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_ids: options.userIds, theme_id: options.themeId, html: options.html, subject: options.subject, notification_category_name: options.notificationCategoryName, template_id: options.templateId, variables: options.variables, }), }, null, ); - return Result.ok(undefined); + return;And update the signature to:
async sendEmail(options: { userIds: string[], themeId?: string | null | false, html?: string, subject?: string, notificationCategoryName?: string, templateId?: string, variables?: Record<string, any>, }): Promise<void> // throws KnownErrors.RequiresCustomEmailServer | KnownErrors.SchemaError | KnownErrors.UserIdDoesNotExistapps/backend/src/route-handlers/smart-request.tsx (1)
176-181
: Require access-type when using workflow tokens.If only x-stack-workflow-token is supplied (no keys), the current eitherKeyOrToken check won’t enforce x-stack-access-type. Include workflowToken to make header validation consistent.
Apply:
- const eitherKeyOrToken = !!(publishableClientKey || secretServerKey || superSecretAdminKey || adminAccessToken); + const eitherKeyOrToken = !!(publishableClientKey || secretServerKey || superSecretAdminKey || adminAccessToken || workflowToken);
♻️ Duplicate comments (5)
apps/backend/src/lib/freestyle.tsx (1)
2-5
: Nice: replaced raw JSON.parse with parseJson and Result flowPrevents accidental throws in error paths and aligns with Result usage.
apps/backend/prisma/schema.prisma (1)
804-821
: Enforce output XOR error; cascade on execution delete; add recent-by-execution index.
- Add onDelete: Cascade on execution to avoid orphans.
- Add DB CHECK to ensure exactly one of output or error is set.
- Add index for recent triggers per execution.
Prisma schema and migration changes:
model WorkflowTrigger { @@ - execution WorkflowExecution @relation(fields: [tenancyId, executionId], references: [tenancyId, id]) + execution WorkflowExecution @relation(fields: [tenancyId, executionId], references: [tenancyId, id], onDelete: Cascade) @@id([tenancyId, executionId, id]) + @@index([tenancyId, executionId, createdAt(sort: Desc)], name: "WorkflowTrigger_byExecution_recent") }Migration SQL (add to the corresponding migration):
ALTER TABLE "WorkflowTrigger" ADD CONSTRAINT "workflowtrigger_output_xor_error" CHECK ( (("output" IS NULL) <> ("error" IS NULL)) );apps/backend/src/lib/workflows.tsx (3)
21-23
: Type union missing "compile".registeredTriggers can include "compile" (see tests). Expand the union to avoid unsafe casts.
Apply:
-type WorkflowRegisteredTriggerType = "sign-up"; +type WorkflowRegisteredTriggerType = "compile" | "sign-up";
340-384
: Preserve trigger/execution IDs and return them to callers.You generate triggerId/executionId but discard them. Return both for persistence and correlation.
Apply:
-async function triggerWorkflowRaw(tenancy: Tenancy, compiledWorkflowCode: string, trigger: WorkflowTrigger): Promise<Result<unknown, string>> { +async function triggerWorkflowRaw( + tenancy: Tenancy, + compiledWorkflowCode: string, + trigger: WorkflowTrigger, +): Promise<Result<{ result: unknown, triggerId: string, executionId: string }, string>> { @@ - return Result.map(freestyleRes, (data) => data.result); + return Result.map(freestyleRes, (data) => ({ + result: data.result, + triggerId, + executionId, + }));
386-416
: Incorrect Execution association (bug) and missing ID persistence.You connectOrCreate WorkflowExecution using compiledWorkflow.id as the execution ID — that’s wrong and collapses all runs into one execution. Use the IDs from triggerWorkflowRaw; persist them.
Apply:
if (compiledWorkflow.compiledCode === null) { return Result.error(`Workflow ${compiledWorkflow.id} failed to compile: ${compiledWorkflow.compileError}`); } - const res = await triggerWorkflowRaw(tenancy, compiledWorkflow.compiledCode, trigger); - if (res.status === "error") { - console.log(`Compiled workflow failed to process trigger: ${res.error}`, { trigger, compiledWorkflowId: compiledWorkflow.id, res }); - } - const prisma = await getPrismaClientForTenancy(tenancy); - await prisma.workflowTrigger.create({ - data: { - triggerData: trigger, - ...(res.status === "ok" ? { output: (res.data ?? Prisma.JsonNull) as any } : { error: res.error }), - execution: { - connectOrCreate: { - where: { - tenancyId_id: { - tenancyId: tenancy.id, - id: compiledWorkflow.id, - }, - }, - create: { - tenancyId: tenancy.id, - compiledWorkflowId: compiledWorkflow.id, - }, - }, - }, - }, - }); + const res = await triggerWorkflowRaw(tenancy, compiledWorkflow.compiledCode, trigger); + const prisma = await getPrismaClientForTenancy(tenancy); + if (res.status === "ok") { + await prisma.workflowTrigger.create({ + data: { + tenancyId: tenancy.id, + id: res.data.triggerId, + triggerData: trigger, + output: (res.data.result ?? Prisma.JsonNull) as any, + execution: { + create: { + tenancyId: tenancy.id, + id: res.data.executionId, + compiledWorkflowId: compiledWorkflow.id, + }, + }, + }, + }); + } else { + console.log(`Compiled workflow failed to process trigger: ${res.error}`, { trigger, compiledWorkflowId: compiledWorkflow.id, res }); + await prisma.workflowTrigger.create({ + data: { + tenancyId: tenancy.id, + id: generateUuid(), + triggerData: trigger, + error: res.error, + execution: { + create: { + tenancyId: tenancy.id, + id: generateUuid(), + compiledWorkflowId: compiledWorkflow.id, + }, + }, + }, + }); + }
🧹 Nitpick comments (14)
packages/template/src/lib/stack-app/apps/interfaces/server-app.ts (1)
52-53
: No breaking call-site changes needed; add JSDoc for thrown errors
All usages simplyawait sendEmail(...)
and none inspect a result wrapper. Update the interface to document thrown error types:- sendEmail(options: SendEmailOptions): Promise<void>, + /** + * Sends email to one or more users. + * Throws on failure (e.g. RequiresCustomEmailServer, SchemaError, UserIdDoesNotExist). + */ + sendEmail(options: SendEmailOptions): Promise<void>,packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (1)
1119-1121
: sendEmail now awaits and returns void — align consumer expectations and add JSDoc here tooMatches the interface change; ensure no callers rely on a Result. Consider adding a short JSDoc here mirroring the interface to list notable thrown errors.
Run the same repository-wide checks suggested in the interface comment to update call sites.
- async sendEmail(options: SendEmailOptions): Promise<void> { + /** + * Throws on delivery/config/schema/user errors propagated by the backend. + */ + async sendEmail(options: SendEmailOptions): Promise<void> { await this._interface.sendEmail(options); }docker/dependencies/docker.compose.yaml (1)
178-184
: Fix stale port in comment and keep config self-explanatoryThe comment still points to 8119 while the mapping exposes 8080 on 8122. Suggest correcting to avoid confusion.
- - "8122:8080" # POST http://localhost:8119/execute/v1/script + - "8122:8080" # POST http://localhost:8122/execute/v1/scriptapps/e2e/tests/js/email.test.ts (1)
32-37
: Use resolves.toBeUndefined() (or just await) instead of resolves.not.toThrow()toThrow() is a matcher for functions, not resolved values; the current assertions pass vacuously. Assert resolution explicitly or simply await the call.
- await expect(serverApp.sendEmail({ + await expect(serverApp.sendEmail({ userIds: [user.id], html: "<h1>Test Email</h1><p>This is a test email with HTML content.</p>", subject: "Test Subject", - })).resolves.not.toThrow(); + })).resolves.toBeUndefined();- await expect(serverApp.sendEmail({ + await expect(serverApp.sendEmail({ userIds: [user.id], templateId: DEFAULT_TEMPLATE_IDS.sign_in_invitation, variables: { teamDisplayName: "Test Team", signInInvitationLink: "https://example.com", }, subject: "Welcome!", - })).resolves.not.toThrow(); + })).resolves.toBeUndefined();- await expect(serverApp.sendEmail({ + await expect(serverApp.sendEmail({ userIds: [user1.id, user2.id], html: "<p>Bulk email test</p>", subject: "Bulk Email Test", - })).resolves.not.toThrow(); + })).resolves.toBeUndefined();- await expect(serverApp.sendEmail({ + await expect(serverApp.sendEmail({ userIds: [user.id], html: "<p>Themed email test</p>", subject: "Themed Email", themeId: DEFAULT_EMAIL_THEME_ID, - })).resolves.not.toThrow(); + })).resolves.toBeUndefined();- await expect(serverApp.sendEmail({ + await expect(serverApp.sendEmail({ userIds: [user.id], html: "<p>Notification email test</p>", subject: "Notification Email", notificationCategoryName: "Transactional", - })).resolves.not.toThrow(); + })).resolves.toBeUndefined();Alternatively:
// simpler and reads well await serverApp.sendEmail({...});Also applies to: 48-57, 73-78, 89-95, 106-112
apps/e2e/tests/backend/workflows.test.ts (4)
5-28
: Consider isolating email server config to a shared helper to reduce repetition and coupling to these testsconfigureEmailAndWorkflow mixes workflow registration with SMTP setup; moving SMTP config to a project bootstrap helper will shorten tests and avoid accidental changes to SMTP defaults when only workflows are intended.
30-38
: Make mailbox wait configurable and surface debug info for flakesHard-coded 10×1s retries can be tight under CI load. Add options for attempts/interval and include last message count in the error.
Apply this diff:
-async function waitForMailboxSubject(mailbox: Mailbox, subject: string) { - for (let i = 0; i < 10; i++) { - const messages = await mailbox.fetchMessages(); - const message = messages.find((m) => m.subject === subject); - if (message) return; - await wait(1_000); - } - throw new Error(`Message with subject ${subject} not found after 10 tries`); +async function waitForMailboxSubject( + mailbox: Mailbox, + subject: string, + { attempts = 20, intervalMs = 1_000 }: { attempts?: number; intervalMs?: number } = {}, +) { + let lastCount = 0; + for (let i = 0; i < attempts; i++) { + const messages = await mailbox.fetchMessages(); + lastCount = messages.length; + if (messages.some((m) => m.subject === subject)) return; + await wait(intervalMs); + } + throw new Error(`Message with subject "${subject}" not found after ${attempts} tries (last mailbox size=${lastCount})`); }
137-137
: Replace fixed sleeps with polling to reduce flakiness and cut test timeStatic waits (12s) slow the suite and are brittle. Prefer polling until a condition is (not) met with a max timeout.
You can add this helper near the top of the file:
async function waitUntil(check: () => Promise<boolean>, { timeoutMs = 15_000, intervalMs = 500 } = {}) { const start = Date.now(); for (;;) { if (await check()) return; if (Date.now() - start >= timeoutMs) throw new Error("waitUntil: timeout"); await wait(intervalMs); } }Example replacement for Line 231:
await waitUntil(async () => { const me = await niceBackendFetch("/api/v1/users/me", { accessType: "server" }); return me.body.is_anonymous === false && me.body.server_metadata?.[markerKey] === me.body.primary_email; });Also applies to: 223-223, 231-231, 257-257, 275-275
161-170
: Tiny safety nit: keep the “bad compile” source minimally valid TSUsing
return return
is effective but can confuse tooling/highlighters. A shorter invalid snippet like@
orclass {
avoids any accidental lexing differences across TS versions.apps/backend/prisma/migrations/20250903003239_workflow_triggers/migration.sql (1)
1-15
: Add index for expiry-based cleanup and consider FK to tenancy for integrity
- Create an index on (tenancyId, expiresAt) to speed up background deletions of expired tokens.
- If your convention allows, add a foreign key to the tenancy table with ON DELETE CASCADE to prevent orphans. If FKs are intentionally avoided in this schema, ignore.
Suggested additions:
-- Speeds up pruning jobs CREATE INDEX "WorkflowTriggerToken_tenancyId_expiresAt_idx" ON "WorkflowTriggerToken"("tenancyId", "expiresAt"); -- If you use FKs for tenancy (adjust table/column names to your schema) -- ALTER TABLE "WorkflowTriggerToken" -- ADD CONSTRAINT "WorkflowTriggerToken_tenancy_fkey" -- FOREIGN KEY ("tenancyId") REFERENCES "Tenancy"("id") ON DELETE CASCADE;apps/backend/src/route-handlers/smart-request.tsx (2)
278-280
: Minor: fold “rare path” into bundled queries later.As noted by the TODO, moving this to rawQueryAll would remove one extra roundtrip. Not urgent.
171-175
: Optional: add telemetry around workflow-token auth.A span or attributes (e.g., has-workflow-token: true, tenancyId) would help observability for these requests.
apps/backend/prisma/schema.prisma (1)
823-837
: Execution relation hygiene and lookup speed.
- Consider onDelete: Cascade from CompiledWorkflow to WorkflowExecution.
- Add index on compiledWorkflowId for quick fetches per compiled workflow (e.g., dashboards).
Apply:
model WorkflowExecution { @@ - compiledWorkflow CompiledWorkflow @relation(fields: [tenancyId, compiledWorkflowId], references: [tenancyId, id]) + compiledWorkflow CompiledWorkflow @relation(fields: [tenancyId, compiledWorkflowId], references: [tenancyId, id], onDelete: Cascade) @@ @@id([tenancyId, id]) + @@index([tenancyId, compiledWorkflowId], name: "WorkflowExecution_byCompiledWorkflow") }apps/backend/src/lib/workflows.tsx (2)
17-19
: Pin external package versions for reproducible builds.'latest' is volatile. Use a pinned version (from lockfile/config), or inject via config.
324-335
: Observability for compile timeouts/cleanup.Consider logging retryInfo and adding metrics (count of deleted stale rows) to ease debugging intermittent compile stalls.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (17)
apps/backend/prisma/migrations/20250903003239_workflow_triggers/migration.sql
(1 hunks)apps/backend/prisma/schema.prisma
(2 hunks)apps/backend/src/lib/email-rendering.tsx
(1 hunks)apps/backend/src/lib/freestyle.tsx
(2 hunks)apps/backend/src/lib/workflows.tsx
(1 hunks)apps/backend/src/middleware.tsx
(1 hunks)apps/backend/src/route-handlers/smart-request.tsx
(5 hunks)apps/backend/src/utils/vercel.tsx
(1 hunks)apps/e2e/tests/backend/workflows.test.ts
(1 hunks)apps/e2e/tests/js/email.test.ts
(8 hunks)docker/dependencies/docker.compose.yaml
(1 hunks)packages/stack-shared/src/interface/server-interface.ts
(1 hunks)packages/stack-shared/src/known-errors.tsx
(2 hunks)packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
(2 hunks)packages/template/src/lib/stack-app/apps/implementations/common.ts
(3 hunks)packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
(3 hunks)packages/template/src/lib/stack-app/apps/interfaces/server-app.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- apps/backend/src/utils/vercel.tsx
- packages/template/src/lib/stack-app/apps/implementations/common.ts
- apps/backend/src/lib/email-rendering.tsx
🧰 Additional context used
📓 Path-based instructions (3)
packages/template/**
📄 CodeRabbit inference engine (AGENTS.md)
When modifying the SDK copies, make changes in packages/template (source of truth)
Files:
packages/template/src/lib/stack-app/apps/interfaces/server-app.ts
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Prefer ES6 Map over Record when representing key–value collections
Files:
packages/template/src/lib/stack-app/apps/interfaces/server-app.ts
apps/backend/src/middleware.tsx
apps/e2e/tests/backend/workflows.test.ts
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
apps/backend/src/route-handlers/smart-request.tsx
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
packages/stack-shared/src/known-errors.tsx
apps/e2e/tests/js/email.test.ts
apps/backend/src/lib/freestyle.tsx
apps/backend/src/lib/workflows.tsx
packages/stack-shared/src/interface/server-interface.ts
**/*.test.{ts,tsx,js}
📄 CodeRabbit inference engine (AGENTS.md)
In tests, prefer .toMatchInlineSnapshot where possible; refer to snapshot-serializer.ts for snapshot formatting and handling of non-deterministic values
Files:
apps/e2e/tests/backend/workflows.test.ts
apps/e2e/tests/js/email.test.ts
🧬 Code graph analysis (9)
packages/template/src/lib/stack-app/apps/interfaces/server-app.ts (1)
packages/template/src/lib/stack-app/email/index.ts (1)
SendEmailOptions
(10-22)
apps/e2e/tests/backend/workflows.test.ts (3)
apps/e2e/tests/helpers.ts (1)
Mailbox
(199-224)packages/stack-shared/src/utils/promises.tsx (1)
wait
(260-268)apps/e2e/tests/backend/backend-helpers.ts (3)
bumpEmailAddress
(172-180)createMailbox
(58-65)niceBackendFetch
(107-166)
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (2)
packages/template/src/lib/stack-app/apps/implementations/common.ts (1)
getDefaultExtraRequestHeaders
(83-85)packages/template/src/lib/stack-app/email/index.ts (1)
SendEmailOptions
(10-22)
apps/backend/src/route-handlers/smart-request.tsx (4)
packages/stack-shared/src/known-errors.tsx (2)
KnownErrors
(1570-1572)KnownErrors
(1574-1696)packages/stack-shared/src/utils/errors.tsx (1)
StackAssertionError
(69-85)apps/backend/src/lib/workflows.tsx (1)
hashWorkflowTriggerToken
(54-59)apps/backend/src/prisma-client.tsx (1)
globalPrismaClient
(31-31)
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (1)
packages/template/src/lib/stack-app/apps/implementations/common.ts (1)
getDefaultExtraRequestHeaders
(83-85)
packages/stack-shared/src/known-errors.tsx (1)
packages/stack-shared/src/index.ts (1)
KnownError
(11-11)
apps/e2e/tests/js/email.test.ts (3)
packages/stack-shared/src/helpers/emails.ts (2)
DEFAULT_TEMPLATE_IDS
(152-158)DEFAULT_EMAIL_THEME_ID
(105-105)apps/e2e/tests/js/js-helpers.ts (1)
createApp
(40-77)packages/stack-shared/src/known-errors.tsx (2)
KnownErrors
(1570-1572)KnownErrors
(1574-1696)
apps/backend/src/lib/freestyle.tsx (3)
packages/stack-shared/src/utils/env.tsx (2)
getEnvVariable
(16-58)getNodeEnvironment
(65-67)packages/stack-shared/src/utils/errors.tsx (1)
StackAssertionError
(69-85)packages/stack-shared/src/utils/json.tsx (1)
parseJson
(72-74)
apps/backend/src/lib/workflows.tsx (12)
packages/stack-shared/src/utils/bytes.tsx (1)
encodeBase64
(114-116)packages/stack-shared/src/utils/crypto.tsx (2)
hash
(111-113)generateSecureRandomString
(20-26)packages/stack-shared/src/utils/esbuild.tsx (1)
bundleJavaScript
(33-128)apps/backend/src/lib/tenancies.tsx (1)
Tenancy
(47-47)packages/stack-shared/src/utils/errors.tsx (4)
StackAssertionError
(69-85)captureError
(126-134)errorToNiceString
(89-92)throwErr
(10-19)packages/stack-shared/src/utils/promises.tsx (3)
timeout
(390-396)wait
(260-268)runAsynchronously
(343-366)packages/stack-shared/src/utils/arrays.tsx (1)
isStringArray
(205-207)apps/backend/src/prisma-client.tsx (3)
getPrismaClientForTenancy
(51-53)retryTransaction
(124-193)globalPrismaClient
(31-31)apps/backend/src/utils/vercel.tsx (2)
allPromisesAndWaitUntilEach
(11-16)runAsynchronouslyAndWaitUntil
(5-9)packages/stack-shared/src/utils/uuids.tsx (1)
generateUuid
(3-8)apps/backend/src/lib/freestyle.tsx (1)
Freestyle
(8-48)packages/stack-shared/src/utils/env.tsx (1)
getEnvVariable
(16-58)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
- GitHub Check: build (22.x)
- GitHub Check: lint_and_build (latest)
- GitHub Check: docker
- GitHub Check: all-good
- GitHub Check: setup-tests
- GitHub Check: build (22.x)
- GitHub Check: docker
- GitHub Check: restart-dev-and-test
- GitHub Check: Security Check
🔇 Additional comments (10)
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (1)
35-36
: Verify default extraRequestHeaders behavior across all app implementations
Confirmed usage ofgetDefaultExtraRequestHeaders
in server-app-impl.ts (ln 269), admin-app-impl.ts (ln 79) and client-app-impl.ts (ln 320). After hardeningcommon.ts
, test each constructor with missing/invalid env vars.apps/backend/src/middleware.tsx (1)
29-29
: CORS: header addition looks goodAllowing x-stack-workflow-token for preflights is correct and aligns with the new auth flow.
apps/e2e/tests/js/email.test.ts (1)
114-127
: Negative-path assertions look correctrejects.toThrow on specific KnownErrors matches the new throwing semantics for sendEmail.
Also applies to: 134-139, 150-154, 165-171
packages/stack-shared/src/known-errors.tsx (1)
1550-1568
: New workflow token errors: LGTMCodes are unique, messages clear, and errors are wired into the export map.
Also applies to: 1694-1696
apps/backend/src/lib/freestyle.tsx (2)
14-19
: Mock key guard looks goodRestricting the mock key to dev/test with a clear assertion is appropriate.
11-13
: Make the Freestyle constructor read the env var safely
- Replace the throwing
getEnvVariable("STACK_FREESTYLE_API_KEY")
call with a non-throwing lookup (e.g.process.env.STACK_FREESTYLE_API_KEY
) or extendgetEnvVariable
to accept a default.- Ensure the two call sites in
email-rendering.tsx
andworkflows.tsx
(bothnew Freestyle()
) no longer throw if neitheroptions.apiKey
nor the env var is set.apps/backend/prisma/migrations/20250903003239_workflow_triggers/migration.sql (1)
6-8
: Confirm updatedAt handling matches Prisma schemaupdatedAt has no default here; ensure the Prisma model uses @updatedat so inserts/updates always set it. Otherwise, consider DEFAULT CURRENT_TIMESTAMP for inserts.
apps/backend/src/route-handlers/smart-request.tsx (2)
9-9
: Import looks correct and scoped.hashWorkflowTriggerToken is the right dependency here. No issues.
282-284
: Deny admin with workflow tokens (good).Explicitly throwing AdminAuthenticationRequired here is correct.
apps/backend/prisma/schema.prisma (1)
27-28
: No-op change.stripeAccountId whitespace-only; nothing to do.
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
apps/backend/prisma/migrations/20250903062546_more_workflow_tokens/migration.sql
(1 hunks)apps/backend/prisma/schema.prisma
(2 hunks)apps/backend/src/lib/workflows.tsx
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/backend/src/lib/workflows.tsx
- apps/backend/prisma/schema.prisma
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Security Check
🔇 Additional comments (2)
apps/backend/prisma/migrations/20250903062546_more_workflow_tokens/migration.sql (2)
9-10
: Multi-tenant correctness: ensure cross-tenant integrityIf
WorkflowTriggerToken
and targets are tenant-scoped, enforce tenant alignment (e.g., via composite FKs or triggers) to prevent cross-tenant linkage.Please verify the schema has a tenant/org/project key on all three tables and that linkage can’t cross tenants. If not, add appropriate constraints or a trigger.
1-7
: Plan backfill and rollout order (expand/contract) to avoid downtime
- Step 1 (this PR): add nullable columns + FKs + indexes.
- Step 2: ship code that writes
triggerId
and, when available,executionId
.- Step 3: backfill
triggerId
for existing rows; leaveexecutionId
null where no execution exists.- Step 4: enforce
triggerId
NOT NULL. Consider leavingexecutionId
nullable long-term.No matches were found when searching for FK or index creation on
executionId
/triggerId
in other migrations—please manually verify that no existing migrations add these constraints or indexes.
apps/backend/prisma/migrations/20250903062546_more_workflow_tokens/migration.sql
Outdated
Show resolved
Hide resolved
apps/backend/prisma/migrations/20250903062546_more_workflow_tokens/migration.sql
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
♻️ Duplicate comments (5)
apps/backend/prisma/schema.prisma (4)
789-802
: Index tokens by expiry for fast cleanup.
Add tenancy-scoped index on expiresAt to speed deletion of expired tokens during sweeps.Apply:
model WorkflowTriggerToken { @@ @@id([tenancyId, id]) @@unique([tenancyId, tokenHash]) + @@index([tenancyId, expiresAt], name: "WorkflowTriggerToken_expiry") }
846-858
: Add index for stale compile cleanup.
Needed for deleteMany startedCompilingAt < now()-20s to avoid scans.model CurrentlyCompilingWorkflow { @@ @@id([tenancyId, workflowId, compilationVersion, sourceHash]) + @@index([tenancyId, startedCompilingAt], name: "CurrentlyCompilingWorkflow_startedAt") }
804-829
: Harden WorkflowTrigger integrity (FK action, state invariant, query index).
- Add onDelete: Cascade on execution to avoid orphans.
- Consider onDelete: SetNull on compiledWorkflow to allow pruning old compiles.
- Enforce exactly one of [output, error] at DB level.
- Add recent-by-execution index for common reads.
Apply:
model WorkflowTrigger { @@ - compiledWorkflow CompiledWorkflow? @relation(fields: [tenancyId, compiledWorkflowId], references: [tenancyId, id]) + compiledWorkflow CompiledWorkflow? @relation(fields: [tenancyId, compiledWorkflowId], references: [tenancyId, id], onDelete: SetNull) @@ - execution WorkflowExecution @relation(fields: [tenancyId, executionId], references: [tenancyId, id]) + execution WorkflowExecution @relation(fields: [tenancyId, executionId], references: [tenancyId, id], onDelete: Cascade) @@ - @@id([tenancyId, id]) + @@id([tenancyId, id]) + @@index([tenancyId, executionId, createdAt(sort: Desc)], name: "WorkflowTrigger_byExecution_recent") }And add a migration CHECK:
ALTER TABLE "WorkflowTrigger" ADD CONSTRAINT "workflowtrigger_output_xor_error" CHECK (("output" IS NULL) <> ("error" IS NULL));
860-881
: Enforce compiledCode XOR compileError.
Add DB CHECK so they’re mutually exclusive.Migration example:
ALTER TABLE "CompiledWorkflow" ADD CONSTRAINT "compiledworkflow_code_xor_error" CHECK (("compiledCode" IS NULL) <> ("compileError" IS NULL));apps/backend/src/lib/workflows.tsx (1)
134-167
: Validate registeredTriggers at runtime.
Filter to known keys ("compile" | "callback" | "sign-up") to avoid garbage values from user code.- return Result.ok({ - compiledCode: compiledCodeResult.data, - registeredTriggers: registeredTriggers, - }); + const valid = (registeredTriggers as string[]).filter(t => t === "compile" || t === "callback" || t === "sign-up"); + return Result.ok({ compiledCode: compiledCodeResult.data, registeredTriggers: valid });
🧹 Nitpick comments (9)
apps/backend/prisma/schema.prisma (1)
831-844
: Optional: add read indexes for executions.
If you frequently fetch executions by workflowId or recency, add:model WorkflowExecution { @@ @@id([tenancyId, id]) + @@index([tenancyId, workflowId, createdAt(sort: Desc)], name: "WorkflowExecution_byWorkflow_recent") }
apps/e2e/tests/backend/workflows.test.ts (1)
30-38
: Reduce flakiness in mailbox polling.
Increase attempts or make timeout configurable; 10s can be tight in CI.-async function waitForMailboxSubject(mailbox: Mailbox, subject: string) { - for (let i = 0; i < 10; i++) { +async function waitForMailboxSubject(mailbox: Mailbox, subject: string, tries = 20, delayMs = 1000) { + for (let i = 0; i < tries; i++) { const messages = await mailbox.fetchMessages(); const message = messages.find((m) => m.subject === subject); if (message) return; - await wait(1_000); + await wait(delayMs); } throw new Error(`Message with subject ${subject} not found after 10 tries`); }apps/backend/src/lib/workflows.tsx (2)
504-515
: Handle per-workflow trigger errors without short-circuiting others.
Wrap each trigger in try/catch or use Promise.allSettled and log failures.- const promises = [...compiledWorkflows] + const promises = [...compiledWorkflows] .filter(([_, compiledWorkflow]) => compiledWorkflow.registeredTriggers.includes(trigger.type)) .map(async ([workflowId, compiledWorkflow]) => { const dbTrigger = await createScheduledTrigger(tenancy, workflowId, trigger, new Date()); - await triggerWorkflow(tenancy, compiledWorkflow, dbTrigger.id); + try { + await triggerWorkflow(tenancy, compiledWorkflow, dbTrigger.id); + } catch (e) { + captureError("workflow-trigger-exception", new StackAssertionError("Failed to trigger workflow", { cause: e, workflowId, trigger })); + } }); - await Promise.all(promises); + await Promise.allSettled(promises);
500-502
: Stubbed scheduler.
Implement polling of WorkflowTrigger where scheduledAt <= now and compiledWorkflowId is null, then dispatch via triggerWorkflow.I can draft a minimal implementation that batches by tenancy and rate-limits; want me to open a follow-up PR?
apps/backend/prisma/migrations/20250903080405_workflows/migration.sql (5)
2-11
: Add index for expirations and a simple validity checkLikely hot paths: sweeping expired tokens and validating TTLs. Add an index on (tenancyId, expiresAt) and a CHECK to ensure expiresAt > createdAt.
Apply:
CREATE TABLE "WorkflowTriggerToken" ( "tenancyId" UUID NOT NULL, "id" UUID NOT NULL, "tokenHash" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, "expiresAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "WorkflowTriggerToken_pkey" PRIMARY KEY ("tenancyId","id") ); + +-- CreateIndex: speed up expiry sweeps +CREATE INDEX "WorkflowTriggerToken_tenancyId_expiresAt_idx" + ON "WorkflowTriggerToken"("tenancyId","expiresAt"); + +-- Guardrail: expiresAt must be after createdAt +ALTER TABLE "WorkflowTriggerToken" + ADD CONSTRAINT "WorkflowTriggerToken_expiresAt_after_createdAt_chk" + CHECK ("expiresAt" > "createdAt");
14-27
: Index the scheduler and join keys; prevent double outcomeScheduling/execution queries will scan by scheduledAt/executionId/compiledWorkflowId. Add targeted indexes and a CHECK so both output and error aren’t set simultaneously.
Apply:
CREATE TABLE "WorkflowTrigger" ( "tenancyId" UUID NOT NULL, "id" UUID NOT NULL, "executionId" UUID NOT NULL, "triggerData" JSONB NOT NULL, "scheduledAt" TIMESTAMP(3), "output" JSONB, "error" JSONB, "compiledWorkflowId" UUID, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "WorkflowTrigger_pkey" PRIMARY KEY ("tenancyId","id") ); + +-- Due scheduling (only pending items) +CREATE INDEX "WorkflowTrigger_tenancyId_scheduledAt_idx" + ON "WorkflowTrigger"("tenancyId","scheduledAt") + WHERE "scheduledAt" IS NOT NULL AND "output" IS NULL AND "error" IS NULL; + +-- Fast joins +CREATE INDEX "WorkflowTrigger_tenancyId_executionId_idx" + ON "WorkflowTrigger"("tenancyId","executionId"); +CREATE INDEX "WorkflowTrigger_tenancyId_compiledWorkflowId_idx" + ON "WorkflowTrigger"("tenancyId","compiledWorkflowId") + WHERE "compiledWorkflowId" IS NOT NULL; + +-- Guardrail: not both output and error +ALTER TABLE "WorkflowTrigger" + ADD CONSTRAINT "WorkflowTrigger_output_or_error_chk" + CHECK (("output" IS NULL) OR ("error" IS NULL));
55-69
: Add a “latest per workflow” indexFetching the latest compiled blob typically needs (tenant, workflowId, compilationVersion DESC). The existing uniqueness on (tenant, workflowId, compilationVersion, sourceHash) is great; add a covering index for recency queries.
Apply:
CREATE TABLE "CompiledWorkflow" ( "tenancyId" UUID NOT NULL, "id" UUID NOT NULL, "workflowId" TEXT NOT NULL, "compilationVersion" INTEGER NOT NULL, "sourceHash" TEXT NOT NULL, "compiledCode" TEXT, "compileError" TEXT, "compiledAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "registeredTriggers" TEXT[], "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "CompiledWorkflow_pkey" PRIMARY KEY ("tenancyId","id") ); + +CREATE INDEX "CompiledWorkflow_tenancyId_workflowId_compilationVersion_idx" + ON "CompiledWorkflow"("tenancyId","workflowId","compilationVersion" DESC);
6-7
: Who updates “updatedAt”? Add DB-side default/trigger or ensure app disciplineupdatedAt is NOT NULL without a default. Confirm your ORM sets it on every write; otherwise writes will fail. If you want DB enforcement, add a trigger to auto-touch updatedAt.
I can supply a Postgres trigger function/migration if you want DB-managed updatedAt.
Also applies to: 24-25, 36-37, 49-50, 66-67
64-65
: Consider normalizing registeredTriggersIf you need queries by trigger value or to enforce referential integrity, move registeredTriggers TEXT[] into a join table. Keep as array only if write-once/read-all patterns dominate.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
apps/backend/prisma/migrations/20250903080405_workflows/migration.sql
(1 hunks)apps/backend/prisma/schema.prisma
(2 hunks)apps/backend/src/lib/workflows.tsx
(1 hunks)apps/e2e/tests/backend/workflows.test.ts
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Prefer ES6 Map over Record when representing key–value collections
Files:
apps/backend/src/lib/workflows.tsx
apps/e2e/tests/backend/workflows.test.ts
**/*.test.{ts,tsx,js}
📄 CodeRabbit inference engine (AGENTS.md)
In tests, prefer .toMatchInlineSnapshot where possible; refer to snapshot-serializer.ts for snapshot formatting and handling of non-deterministic values
Files:
apps/e2e/tests/backend/workflows.test.ts
🧬 Code graph analysis (2)
apps/backend/src/lib/workflows.tsx (11)
packages/stack-shared/src/utils/bytes.tsx (1)
encodeBase64
(114-116)packages/stack-shared/src/utils/crypto.tsx (2)
hash
(111-113)generateSecureRandomString
(20-26)packages/stack-shared/src/utils/esbuild.tsx (1)
bundleJavaScript
(33-128)packages/stack-shared/src/utils/errors.tsx (4)
StackAssertionError
(69-85)captureError
(126-134)errorToNiceString
(89-92)throwErr
(10-19)packages/stack-shared/src/utils/promises.tsx (3)
timeout
(390-396)wait
(260-268)runAsynchronously
(343-366)packages/stack-shared/src/utils/arrays.tsx (1)
isStringArray
(205-207)apps/backend/src/prisma-client.tsx (3)
getPrismaClientForTenancy
(51-53)retryTransaction
(124-193)globalPrismaClient
(31-31)apps/backend/src/utils/vercel.tsx (2)
allPromisesAndWaitUntilEach
(11-16)runAsynchronouslyAndWaitUntil
(5-9)apps/backend/src/lib/freestyle.tsx (1)
Freestyle
(8-48)packages/stack-shared/src/utils/env.tsx (1)
getEnvVariable
(16-58)packages/stack-shared/src/utils/uuids.tsx (1)
generateUuid
(3-8)
apps/e2e/tests/backend/workflows.test.ts (3)
apps/e2e/tests/helpers.ts (1)
Mailbox
(199-224)packages/stack-shared/src/utils/promises.tsx (1)
wait
(260-268)apps/e2e/tests/backend/backend-helpers.ts (3)
bumpEmailAddress
(172-180)createMailbox
(58-65)niceBackendFetch
(107-166)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
- GitHub Check: setup-tests
- GitHub Check: docker
- GitHub Check: restart-dev-and-test
- GitHub Check: all-good
- GitHub Check: lint_and_build (latest)
- GitHub Check: docker
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: Security Check
🔇 Additional comments (9)
apps/backend/prisma/schema.prisma (2)
27-27
: LGTM: formatting-only change.
776-787
: LGTM: DataVaultEntry looks correct.
- Composite uniqueness and secondary index make sense for hot paths.
apps/e2e/tests/backend/workflows.test.ts (3)
5-28
: Good test setup helper.
Concise config writer; enables per-test workflow source injection.
138-146
: Nice negative test.
Wait-and-assert-only-verification mail ensures disabled workflows don’t send.
248-289
: Versioning test is clear and effective.
Validates that subsequent compiles take effect without retroactive changes.apps/backend/prisma/migrations/20250903080405_workflows/migration.sql (4)
2-11
: Multi-tenant PK and unique hash look solidComposite PK on (tenancyId, id) and unique (tenancyId, tokenHash) are appropriate for isolation and O(1) lookups by hash.
42-52
: Clarify concurrency semantics for “CurrentlyCompilingWorkflow”If you intend “at most one active compile per workflow per tenant,” consider a unique constraint on (tenancyId, workflowId). If parallel compiles are valid, keep as-is.
Optional index for retrieving the latest attempt:
CREATE INDEX "CurrentlyCompilingWorkflow_tenancyId_workflowId_version_idx" ON "CurrentlyCompilingWorkflow"("tenancyId","workflowId","compilationVersion" DESC);
71-76
: Good: critical uniqueness covered
- Unique (tenancyId, tokenHash) prevents duplicate tokens per tenant.
- Unique (tenancyId, workflowId, compilationVersion, sourceHash) ensures no duplicate artifacts for the same build inputs.
78-81
: Double-check ON DELETE policiesRESTRICT is safe, but verify operational needs:
- Deleting a WorkflowExecution: should its triggers cascade? If yes, switch to ON DELETE CASCADE.
- Deleting a CompiledWorkflow: if old artifacts can be pruned even when triggers exist, consider SET NULL on compiledWorkflowId (it’s nullable) to allow GC.
If desired, adjust:
-ALTER TABLE "WorkflowTrigger" ADD CONSTRAINT "WorkflowTrigger_tenancyId_compiledWorkflowId_fkey" FOREIGN KEY ("tenancyId", "compiledWorkflowId") REFERENCES "CompiledWorkflow"("tenancyId", "id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "WorkflowTrigger" ADD CONSTRAINT "WorkflowTrigger_tenancyId_compiledWorkflowId_fkey" FOREIGN KEY ("tenancyId", "compiledWorkflowId") REFERENCES "CompiledWorkflow"("tenancyId", "id") ON DELETE SET NULL ON UPDATE CASCADE; -ALTER TABLE "WorkflowTrigger" ADD CONSTRAINT "WorkflowTrigger_tenancyId_executionId_fkey" FOREIGN KEY ("tenancyId", "executionId") REFERENCES "WorkflowExecution"("tenancyId", "id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "WorkflowTrigger" ADD CONSTRAINT "WorkflowTrigger_tenancyId_executionId_fkey" FOREIGN KEY ("tenancyId", "executionId") REFERENCES "WorkflowExecution"("tenancyId", "id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE "WorkflowExecution" ( | ||
"tenancyId" UUID NOT NULL, | ||
"id" UUID NOT NULL, | ||
"workflowId" TEXT NOT NULL, | ||
"triggerIds" TEXT[], | ||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
"updatedAt" TIMESTAMP(3) NOT NULL, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Remove triggerIds array to avoid drift; rely on FK from WorkflowTrigger
WorkflowTrigger already has executionId -> WorkflowExecution. Keeping triggerIds TEXT[] duplicates the relationship without FK guarantees and can desynchronize, causing data integrity issues.
Apply (either edit this migration before apply, or follow-up migration to drop the column):
CREATE TABLE "WorkflowExecution" (
"tenancyId" UUID NOT NULL,
"id" UUID NOT NULL,
"workflowId" TEXT NOT NULL,
- "triggerIds" TEXT[],
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "WorkflowExecution_pkey" PRIMARY KEY ("tenancyId","id")
);
Optional: add an index for common lookups:
CREATE INDEX "WorkflowExecution_tenancyId_workflowId_idx"
ON "WorkflowExecution"("tenancyId","workflowId");
🤖 Prompt for AI Agents
In apps/backend/prisma/migrations/20250903080405_workflows/migration.sql around
lines 30-36, remove the redundant "triggerIds" TEXT[] column from the
WorkflowExecution CREATE TABLE statement (or if the migration has already been
applied, create a follow-up migration that drops the triggerIds column) so the
relationship is enforced only via the WorkflowTrigger.executionId FK;
additionally consider adding the suggested composite index on (tenancyId,
workflowId) for common lookups by creating an index named
WorkflowExecution_tenancyId_workflowId_idx.
const registeredCallbacks = new Map(); | ||
globalThis.registerCallback = (callbackId, func) => { | ||
registeredCallbacks.set(callbackId, func); | ||
}; | ||
_registerTrigger("callback", ({ callbackId, data }) => { | ||
const callbackFunc = registeredCallbacks.get(callbackId); | ||
if (!callbackFunc) { | ||
throw new Error(\`Callback \${callbackId} not found. Was it maybe deleted from the workflow?\`); | ||
} | ||
return callbackFunc(JSON.parse(data.dataJson)); | ||
}); | ||
let scheduledCallback = undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: callback data is parsed incorrectly.
scheduleCallback stores opaque data; callback triggers receive it as-is. JSON.parse on data.dataJson will throw.
- _registerTrigger("callback", ({ callbackId, data }) => {
+ _registerTrigger("callback", ({ callbackId, data }) => {
const callbackFunc = registeredCallbacks.get(callbackId);
if (!callbackFunc) {
throw new Error(`Callback ${callbackId} not found. Was it maybe deleted from the workflow?`);
}
- return callbackFunc(JSON.parse(data.dataJson));
+ return callbackFunc(data);
});
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const registeredCallbacks = new Map(); | |
globalThis.registerCallback = (callbackId, func) => { | |
registeredCallbacks.set(callbackId, func); | |
}; | |
_registerTrigger("callback", ({ callbackId, data }) => { | |
const callbackFunc = registeredCallbacks.get(callbackId); | |
if (!callbackFunc) { | |
throw new Error(\`Callback \${callbackId} not found. Was it maybe deleted from the workflow?\`); | |
} | |
return callbackFunc(JSON.parse(data.dataJson)); | |
}); | |
let scheduledCallback = undefined; | |
const registeredCallbacks = new Map(); | |
globalThis.registerCallback = (callbackId, func) => { | |
registeredCallbacks.set(callbackId, func); | |
}; | |
_registerTrigger("callback", ({ callbackId, data }) => { | |
const callbackFunc = registeredCallbacks.get(callbackId); | |
if (!callbackFunc) { | |
throw new Error(`Callback ${callbackId} not found. Was it maybe deleted from the workflow?`); | |
} | |
return callbackFunc(data); | |
}); | |
let scheduledCallback = undefined; |
🤖 Prompt for AI Agents
In apps/backend/src/lib/workflows.tsx around lines 76 to 87, the callback
handler incorrectly does JSON.parse(data.dataJson) even though scheduled
callbacks store opaque data and may already be objects; this causes a runtime
throw. Replace the JSON.parse call with a safe pass-through: if data.dataJson is
a string then parse it, otherwise pass data.dataJson directly (or simply pass
data.dataJson without parsing if the system always stores raw values). Update
the return to call callbackFunc with the resolved value so callbacks get the
original opaque data unchanged.
async function triggerWorkflowRaw(tenancy: Tenancy, compiledWorkflowCode: string, trigger: WorkflowTrigger): Promise<Result<unknown, string>> { | ||
const workflowToken = generateSecureRandomString(); | ||
const workflowTriggerToken = await globalPrismaClient.workflowTriggerToken.create({ | ||
data: { | ||
expiresAt: new Date(Date.now() + 1000 * 35), | ||
tenancyId: tenancy.id, | ||
tokenHash: await hashWorkflowTriggerToken(workflowToken), | ||
}, | ||
}); | ||
|
||
const tokenRefreshInterval = setInterval(() => { | ||
runAsynchronously(async () => { | ||
await globalPrismaClient.workflowTriggerToken.update({ | ||
where: { | ||
tenancyId_id: { | ||
tenancyId: tenancy.id, | ||
id: workflowTriggerToken.id, | ||
}, | ||
}, | ||
data: { expiresAt: new Date(Date.now() + 1000 * 35) }, | ||
}); | ||
}); | ||
}, 10_000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use tenancy-scoped Prisma client for tokens.
Avoid mixing global and tenancy clients; keeps workflow tokens colocated with the tenancy’s source of truth.
- const workflowTriggerToken = await globalPrismaClient.workflowTriggerToken.create({
+ const prisma = await getPrismaClientForTenancy(tenancy);
+ const workflowTriggerToken = await prisma.workflowTriggerToken.create({
@@
- await globalPrismaClient.workflowTriggerToken.update({
+ await prisma.workflowTriggerToken.update({
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
async function triggerWorkflowRaw(tenancy: Tenancy, compiledWorkflowCode: string, trigger: WorkflowTrigger): Promise<Result<unknown, string>> { | |
const workflowToken = generateSecureRandomString(); | |
const workflowTriggerToken = await globalPrismaClient.workflowTriggerToken.create({ | |
data: { | |
expiresAt: new Date(Date.now() + 1000 * 35), | |
tenancyId: tenancy.id, | |
tokenHash: await hashWorkflowTriggerToken(workflowToken), | |
}, | |
}); | |
const tokenRefreshInterval = setInterval(() => { | |
runAsynchronously(async () => { | |
await globalPrismaClient.workflowTriggerToken.update({ | |
where: { | |
tenancyId_id: { | |
tenancyId: tenancy.id, | |
id: workflowTriggerToken.id, | |
}, | |
}, | |
data: { expiresAt: new Date(Date.now() + 1000 * 35) }, | |
}); | |
}); | |
}, 10_000); | |
async function triggerWorkflowRaw( | |
tenancy: Tenancy, | |
compiledWorkflowCode: string, | |
trigger: WorkflowTrigger | |
): Promise<Result<unknown, string>> { | |
const workflowToken = generateSecureRandomString(); | |
const prisma = await getPrismaClientForTenancy(tenancy); | |
const workflowTriggerToken = await prisma.workflowTriggerToken.create({ | |
data: { | |
expiresAt: new Date(Date.now() + 1000 * 35), | |
tenancyId: tenancy.id, | |
tokenHash: await hashWorkflowTriggerToken(workflowToken), | |
}, | |
}); | |
const tokenRefreshInterval = setInterval(() => { | |
runAsynchronously(async () => { | |
await prisma.workflowTriggerToken.update({ | |
where: { | |
tenancyId_id: { | |
tenancyId: tenancy.id, | |
id: workflowTriggerToken.id, | |
}, | |
}, | |
data: { expiresAt: new Date(Date.now() + 1000 * 35) }, | |
}); | |
}); | |
}, 10_000); | |
// …rest of function… | |
} |
🤖 Prompt for AI Agents
In apps/backend/src/lib/workflows.tsx around lines 367 to 389, the code is
creating and refreshing workflow trigger tokens using globalPrismaClient; switch
to the tenancy-scoped Prisma client so tokens are stored with the tenancy’s
data. Replace calls to globalPrismaClient.workflowTriggerToken.create/update
with the tenancy-scoped client (e.g., tenancy.prisma or tenancy.prismaClient—use
whatever property holds the per-tenancy Prisma instance in this codebase),
keeping the same create and update payloads and error handling, and ensure the
interval continuation also uses that tenancy client instead of
globalPrismaClient.
async function createScheduledTrigger(tenancy: Tenancy, workflowId: string, trigger: WorkflowTrigger, scheduledAt: Date) { | ||
const executionId = trigger.type === "callback" ? trigger.executionId : generateUuid(); | ||
|
||
const prisma = await getPrismaClientForTenancy(tenancy); | ||
const dbTrigger = await prisma.workflowTrigger.create({ | ||
data: { | ||
triggerData: trigger as any, | ||
scheduledAt, | ||
execution: { | ||
connectOrCreate: { | ||
where: { | ||
tenancyId_id: { | ||
tenancyId: tenancy.id, | ||
id: executionId, | ||
}, | ||
}, | ||
create: { | ||
tenancyId: tenancy.id, | ||
workflowId, | ||
}, | ||
}, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
connectOrCreate bug: execution.id mismatch.
You generate executionId for connectOrCreate.where but don’t set it in create, causing a mismatch.
connectOrCreate: {
where: {
tenancyId_id: {
tenancyId: tenancy.id,
id: executionId,
},
},
create: {
tenancyId: tenancy.id,
+ id: executionId,
workflowId,
},
},
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
async function createScheduledTrigger(tenancy: Tenancy, workflowId: string, trigger: WorkflowTrigger, scheduledAt: Date) { | |
const executionId = trigger.type === "callback" ? trigger.executionId : generateUuid(); | |
const prisma = await getPrismaClientForTenancy(tenancy); | |
const dbTrigger = await prisma.workflowTrigger.create({ | |
data: { | |
triggerData: trigger as any, | |
scheduledAt, | |
execution: { | |
connectOrCreate: { | |
where: { | |
tenancyId_id: { | |
tenancyId: tenancy.id, | |
id: executionId, | |
}, | |
}, | |
create: { | |
tenancyId: tenancy.id, | |
workflowId, | |
}, | |
}, | |
}, | |
async function createScheduledTrigger( | |
tenancy: Tenancy, | |
workflowId: string, | |
trigger: WorkflowTrigger, | |
scheduledAt: Date | |
) { | |
const executionId = | |
trigger.type === "callback" ? trigger.executionId : generateUuid(); | |
const prisma = await getPrismaClientForTenancy(tenancy); | |
const dbTrigger = await prisma.workflowTrigger.create({ | |
data: { | |
triggerData: trigger as any, | |
scheduledAt, | |
execution: { | |
connectOrCreate: { | |
where: { | |
tenancyId_id: { | |
tenancyId: tenancy.id, | |
id: executionId, | |
}, | |
}, | |
create: { | |
tenancyId: tenancy.id, | |
id: executionId, // ← ensure the new execution uses the intended ID | |
workflowId, | |
}, | |
}, | |
}, | |
}, | |
}); | |
// ...rest of the function | |
} |
🤖 Prompt for AI Agents
In apps/backend/src/lib/workflows.tsx around lines 410 to 431, the
connectOrCreate uses executionId in the where clause but the create object
doesn't set the same id, causing a mismatch; fix by adding id: executionId to
the create payload (and ensure tenancyId and workflowId remain present) so the
connectOrCreate where and create objects reference the same execution id.
compiledWorkflow.id, | ||
{ | ||
type: "callback", | ||
callbackId, | ||
data: callbackData, | ||
scheduledAtMillis: scheduleAt.getTime(), | ||
callerTriggerId: triggerId, | ||
executionId: trigger.executionId, | ||
}, | ||
scheduleAt | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrong workflow identifier for scheduled callbacks.
Pass the logical workflowId, not the compiledWorkflow row id.
- compiledWorkflow.id,
+ compiledWorkflow.workflowId,
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
compiledWorkflow.id, | |
{ | |
type: "callback", | |
callbackId, | |
data: callbackData, | |
scheduledAtMillis: scheduleAt.getTime(), | |
callerTriggerId: triggerId, | |
executionId: trigger.executionId, | |
}, | |
scheduleAt | |
); | |
compiledWorkflow.workflowId, | |
{ | |
type: "callback", | |
callbackId, | |
data: callbackData, | |
scheduledAtMillis: scheduleAt.getTime(), | |
callerTriggerId: triggerId, | |
executionId: trigger.executionId, | |
}, | |
scheduleAt | |
); |
🤖 Prompt for AI Agents
In apps/backend/src/lib/workflows.tsx around lines 469 to 479, the scheduled
callback is being created with compiledWorkflow.id (the compiled row id) but
should use the logical workflowId; replace the compiledWorkflow.id argument with
the logical workflowId variable used earlier in this scope (or the upstream
workflowId parameter) so the scheduled callback references the correct workflow
identifier when calling the scheduling function and ensure the value matches the
expected type/name in that function signature.
Important
Introduces per-project workflows with management capabilities, workflow token authentication, and various improvements and bug fixes.
smart-request.tsx
.WorkflowTriggerToken
,WorkflowTrigger
,WorkflowExecution
,CurrentlyCompilingWorkflow
, andCompiledWorkflow
inmigration.sql
.triggerWorkflows
function inworkflows.tsx
to handle workflow triggers.users/crud.tsx
to trigger workflows on user sign-up.workflows/page.tsx
andworkflows/[workflowId]/page.tsx
.numbers.tsx
.middleware.tsx
.providers.tsx
.This description was created by
for 7adeee4. You can customize this summary. It will automatically update as commits are pushed.
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Tests