From c7d5010f46cbbefd0a493f546cc4a535e53f9982 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Tue, 7 Apr 2026 12:45:17 -0400 Subject: [PATCH 1/2] feat(trigger): add org tags to all trigger job runs Add `await tags.add([`org:${organizationId}`])` to all 25 trigger tasks that receive an organizationId, making it easy to filter and identify which organization a job was run for in the trigger.dev dashboard. Scheduled/bulk tasks that iterate over all orgs are excluded since they don't have a single organizationId. Also updates the vendor extraction prompt to prefer company names over product names (e.g. "Anthropic" not "Claude", "OpenAI" not "ChatGPT"). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/trigger/browser-automation/run-browser-automation.ts | 4 +++- .../api/src/trigger/cloud-security/run-cloud-security-scan.ts | 4 +++- .../src/trigger/integration-platform/run-connection-checks.ts | 4 +++- .../integration-platform/run-task-integration-checks.ts | 4 +++- apps/api/src/trigger/policies/update-policy.ts | 4 +++- apps/api/src/trigger/questionnaire/answer-question.ts | 4 +++- apps/api/src/trigger/questionnaire/parse-questionnaire.ts | 4 +++- .../src/trigger/tasks/onboarding/migrate-policies-for-org.ts | 4 +++- .../vector-store/delete-all-manual-answers-orchestrator.ts | 4 +++- .../trigger/vector-store/delete-knowledge-base-document.ts | 4 +++- apps/api/src/trigger/vector-store/delete-manual-answer.ts | 4 +++- .../trigger/vector-store/process-knowledge-base-document.ts | 4 +++- .../process-knowledge-base-documents-orchestrator.ts | 4 +++- apps/api/src/trigger/vendor/vendor-risk-assessment-task.ts | 4 +++- .../app/src/trigger/tasks/auditor/generate-auditor-content.ts | 3 ++- .../src/trigger/tasks/device/create-fleet-label-for-org.ts | 4 +++- apps/app/src/trigger/tasks/email/new-policy-email.ts | 3 ++- .../app/src/trigger/tasks/email/publish-all-policies-email.ts | 3 ++- apps/app/src/trigger/tasks/email/weekly-task-digest-email.ts | 3 ++- .../src/trigger/tasks/integration/run-integration-tests.ts | 3 ++- .../tasks/onboarding/backfill-training-videos-for-org.ts | 3 ++- .../src/trigger/tasks/onboarding/generate-full-policies.ts | 3 ++- .../src/trigger/tasks/onboarding/generate-risk-mitigation.ts | 4 +++- .../trigger/tasks/onboarding/generate-vendor-mitigation.ts | 4 +++- .../trigger/tasks/onboarding/onboard-organization-helpers.ts | 2 +- apps/app/src/trigger/tasks/onboarding/onboard-organization.ts | 3 ++- 26 files changed, 68 insertions(+), 26 deletions(-) diff --git a/apps/api/src/trigger/browser-automation/run-browser-automation.ts b/apps/api/src/trigger/browser-automation/run-browser-automation.ts index d5b12c616b..5b9bb5a8c6 100644 --- a/apps/api/src/trigger/browser-automation/run-browser-automation.ts +++ b/apps/api/src/trigger/browser-automation/run-browser-automation.ts @@ -1,5 +1,5 @@ import { db } from '@db'; -import { logger, task } from '@trigger.dev/sdk'; +import { logger, tags, task } from '@trigger.dev/sdk'; import { BrowserbaseService } from '../../browserbase/browserbase.service'; import { triggerEmail } from '../../email/trigger-email'; import { TaskStatusChangedEmail } from '../../email/templates/task-status-changed'; @@ -187,6 +187,8 @@ export const runBrowserAutomation = task({ }) => { const { automationId, automationName, organizationId, taskId } = payload; + await tags.add([`org:${organizationId}`]); + logger.info(`Running browser automation "${automationName}"`, { automationId, organizationId, diff --git a/apps/api/src/trigger/cloud-security/run-cloud-security-scan.ts b/apps/api/src/trigger/cloud-security/run-cloud-security-scan.ts index 37b01f96bc..31786c6e9d 100644 --- a/apps/api/src/trigger/cloud-security/run-cloud-security-scan.ts +++ b/apps/api/src/trigger/cloud-security/run-cloud-security-scan.ts @@ -1,5 +1,5 @@ import { db } from '@db'; -import { logger, task } from '@trigger.dev/sdk'; +import { logger, tags, task } from '@trigger.dev/sdk'; /** * Trigger task that runs a cloud security scan for a single connection. @@ -20,6 +20,8 @@ export const runCloudSecurityScan = task({ const { connectionId, organizationId, providerSlug, connectionName } = payload; + await tags.add([`org:${organizationId}`]); + logger.info( `Starting cloud security scan for connection: ${connectionName}`, { diff --git a/apps/api/src/trigger/integration-platform/run-connection-checks.ts b/apps/api/src/trigger/integration-platform/run-connection-checks.ts index e25f12b3ae..9a28d861bf 100644 --- a/apps/api/src/trigger/integration-platform/run-connection-checks.ts +++ b/apps/api/src/trigger/integration-platform/run-connection-checks.ts @@ -1,6 +1,6 @@ import { getManifest, runAllChecks } from '@trycompai/integration-platform'; import { db } from '@db'; -import { logger, task } from '@trigger.dev/sdk'; +import { logger, tags, task } from '@trigger.dev/sdk'; /** * Trigger task that runs all checks for a connection. @@ -22,6 +22,8 @@ export const runConnectionChecks = task({ }) => { const { connectionId, organizationId, providerSlug } = payload; + await tags.add([`org:${organizationId}`]); + logger.info(`Auto-running checks for connection ${connectionId}`, { provider: providerSlug, organizationId, diff --git a/apps/api/src/trigger/integration-platform/run-task-integration-checks.ts b/apps/api/src/trigger/integration-platform/run-task-integration-checks.ts index 36e1c7f50a..24ca17a9ee 100644 --- a/apps/api/src/trigger/integration-platform/run-task-integration-checks.ts +++ b/apps/api/src/trigger/integration-platform/run-task-integration-checks.ts @@ -1,6 +1,6 @@ import { getManifest, runAllChecks } from '@trycompai/integration-platform'; import { db } from '@db'; -import { logger, task } from '@trigger.dev/sdk'; +import { logger, tags, task } from '@trigger.dev/sdk'; import { triggerEmail } from '../../email/trigger-email'; import { TaskStatusChangedEmail } from '../../email/templates/task-status-changed'; import { isUserUnsubscribed } from '@trycompai/email'; @@ -185,6 +185,8 @@ export const runTaskIntegrationChecks = task({ checkIds, } = payload; + await tags.add([`org:${organizationId}`]); + logger.info(`Running integration checks for task "${taskTitle}"`, { taskId, connectionId, diff --git a/apps/api/src/trigger/policies/update-policy.ts b/apps/api/src/trigger/policies/update-policy.ts index 0ddb3e5b32..2e177b6f84 100644 --- a/apps/api/src/trigger/policies/update-policy.ts +++ b/apps/api/src/trigger/policies/update-policy.ts @@ -1,4 +1,4 @@ -import { logger, metadata, queue, schemaTask } from '@trigger.dev/sdk'; +import { logger, metadata, queue, schemaTask, tags } from '@trigger.dev/sdk'; import { z } from 'zod'; import { processPolicyUpdate } from './update-policy-helpers'; @@ -29,6 +29,8 @@ export const updatePolicy = schemaTask({ memberId: z.string().optional(), }), run: async (params) => { + await tags.add([`org:${params.organizationId}`]); + try { logger.info(`Starting policy update for policy ${params.policyId}`); diff --git a/apps/api/src/trigger/questionnaire/answer-question.ts b/apps/api/src/trigger/questionnaire/answer-question.ts index 8c55926ca9..c3828a28f8 100644 --- a/apps/api/src/trigger/questionnaire/answer-question.ts +++ b/apps/api/src/trigger/questionnaire/answer-question.ts @@ -1,5 +1,5 @@ import { syncOrganizationEmbeddings } from '@/vector-store/lib'; -import { logger, metadata, task } from '@trigger.dev/sdk'; +import { logger, metadata, tags, task } from '@trigger.dev/sdk'; import { generateAnswerWithRAG } from './answer-question-helpers'; export interface AnswerQuestionPayload { @@ -177,6 +177,8 @@ export const answerQuestionTask = task({ questionIndex: number; totalQuestions: number; }) => { + await tags.add([`org:${payload.organizationId}`]); + return await answerQuestion(payload); }, }); diff --git a/apps/api/src/trigger/questionnaire/parse-questionnaire.ts b/apps/api/src/trigger/questionnaire/parse-questionnaire.ts index 471bb078fd..9a04a1a916 100644 --- a/apps/api/src/trigger/questionnaire/parse-questionnaire.ts +++ b/apps/api/src/trigger/questionnaire/parse-questionnaire.ts @@ -1,7 +1,7 @@ import { extractS3KeyFromUrl } from '@/app/s3'; import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'; import { db } from '@db'; -import { logger, task } from '@trigger.dev/sdk'; +import { logger, tags, task } from '@trigger.dev/sdk'; // Import shared utilities import { @@ -253,6 +253,8 @@ export const parseQuestionnaireTask = task({ }) => { const taskStartTime = Date.now(); + await tags.add([`org:${payload.organizationId}`]); + logger.info('Starting parse questionnaire task', { inputType: payload.inputType, organizationId: payload.organizationId, diff --git a/apps/api/src/trigger/tasks/onboarding/migrate-policies-for-org.ts b/apps/api/src/trigger/tasks/onboarding/migrate-policies-for-org.ts index 6573cb4235..e5e9cfeb4a 100644 --- a/apps/api/src/trigger/tasks/onboarding/migrate-policies-for-org.ts +++ b/apps/api/src/trigger/tasks/onboarding/migrate-policies-for-org.ts @@ -1,5 +1,5 @@ import { db, PolicyStatus, type Prisma } from '@db'; -import { logger, schemaTask } from '@trigger.dev/sdk'; +import { logger, schemaTask, tags } from '@trigger.dev/sdk'; import { z } from 'zod'; const POLICY_BATCH_SIZE = 50; @@ -10,6 +10,8 @@ export const migratePoliciesForOrg = schemaTask({ organizationId: z.string(), }), run: async ({ organizationId }) => { + await tags.add([`org:${organizationId}`]); + // Find policies without any versions const policiesWithoutVersions = await db.policy.findMany({ where: { diff --git a/apps/api/src/trigger/vector-store/delete-all-manual-answers-orchestrator.ts b/apps/api/src/trigger/vector-store/delete-all-manual-answers-orchestrator.ts index 04a71464a0..3c47cf55ee 100644 --- a/apps/api/src/trigger/vector-store/delete-all-manual-answers-orchestrator.ts +++ b/apps/api/src/trigger/vector-store/delete-all-manual-answers-orchestrator.ts @@ -1,4 +1,4 @@ -import { logger, metadata, task } from '@trigger.dev/sdk'; +import { logger, metadata, tags, task } from '@trigger.dev/sdk'; import { db } from '@db'; import { deleteManualAnswerTask } from './delete-manual-answer'; @@ -17,6 +17,8 @@ export const deleteAllManualAnswersOrchestratorTask = task({ organizationId: string; manualAnswerIds?: string[]; // Optional: IDs passed directly to avoid race condition }) => { + await tags.add([`org:${payload.organizationId}`]); + logger.info('Starting delete all manual answers from vector DB', { organizationId: payload.organizationId, manualAnswerIdsProvided: !!payload.manualAnswerIds, diff --git a/apps/api/src/trigger/vector-store/delete-knowledge-base-document.ts b/apps/api/src/trigger/vector-store/delete-knowledge-base-document.ts index 53043e5e71..269a23ef4b 100644 --- a/apps/api/src/trigger/vector-store/delete-knowledge-base-document.ts +++ b/apps/api/src/trigger/vector-store/delete-knowledge-base-document.ts @@ -1,4 +1,4 @@ -import { logger, task } from '@trigger.dev/sdk'; +import { logger, tags, task } from '@trigger.dev/sdk'; import { findEmbeddingsForSource } from '@/vector-store/lib/core/find-existing-embeddings'; import { vectorIndex } from '@/vector-store/lib/core/client'; import { db } from '@db'; @@ -12,6 +12,8 @@ export const deleteKnowledgeBaseDocumentTask = task({ maxAttempts: 3, }, run: async (payload: { documentId: string; organizationId: string }) => { + await tags.add([`org:${payload.organizationId}`]); + logger.info('Deleting Knowledge Base document from vector DB', { documentId: payload.documentId, organizationId: payload.organizationId, diff --git a/apps/api/src/trigger/vector-store/delete-manual-answer.ts b/apps/api/src/trigger/vector-store/delete-manual-answer.ts index 576568c56d..41b96be4fb 100644 --- a/apps/api/src/trigger/vector-store/delete-manual-answer.ts +++ b/apps/api/src/trigger/vector-store/delete-manual-answer.ts @@ -1,4 +1,4 @@ -import { logger, task } from '@trigger.dev/sdk'; +import { logger, tags, task } from '@trigger.dev/sdk'; import { deleteManualAnswerFromVector } from '@/vector-store/lib/sync/sync-manual-answer'; /** @@ -11,6 +11,8 @@ export const deleteManualAnswerTask = task({ maxAttempts: 3, }, run: async (payload: { manualAnswerId: string; organizationId: string }) => { + await tags.add([`org:${payload.organizationId}`]); + logger.info('Deleting manual answer from vector DB', { manualAnswerId: payload.manualAnswerId, organizationId: payload.organizationId, diff --git a/apps/api/src/trigger/vector-store/process-knowledge-base-document.ts b/apps/api/src/trigger/vector-store/process-knowledge-base-document.ts index 2862d77698..18334c5baf 100644 --- a/apps/api/src/trigger/vector-store/process-knowledge-base-document.ts +++ b/apps/api/src/trigger/vector-store/process-knowledge-base-document.ts @@ -1,4 +1,4 @@ -import { logger, task } from '@trigger.dev/sdk'; +import { logger, tags, task } from '@trigger.dev/sdk'; import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'; import { db } from '@db'; import { batchUpsertEmbeddings } from '@/vector-store/lib/core/upsert-embedding'; @@ -86,6 +86,8 @@ export const processKnowledgeBaseDocumentTask = task({ }, maxDuration: 1000 * 60 * 30, // 30 minutes for large files run: async (payload: { documentId: string; organizationId: string }) => { + await tags.add([`org:${payload.organizationId}`]); + logger.info('Processing Knowledge Base document', { documentId: payload.documentId, organizationId: payload.organizationId, diff --git a/apps/api/src/trigger/vector-store/process-knowledge-base-documents-orchestrator.ts b/apps/api/src/trigger/vector-store/process-knowledge-base-documents-orchestrator.ts index 296b0239e6..7da73e5b26 100644 --- a/apps/api/src/trigger/vector-store/process-knowledge-base-documents-orchestrator.ts +++ b/apps/api/src/trigger/vector-store/process-knowledge-base-documents-orchestrator.ts @@ -1,4 +1,4 @@ -import { logger, metadata, task } from '@trigger.dev/sdk'; +import { logger, metadata, tags, task } from '@trigger.dev/sdk'; import { processKnowledgeBaseDocumentTask } from './process-knowledge-base-document'; const BATCH_SIZE = 10; // Process 10 documents at a time @@ -14,6 +14,8 @@ export const processKnowledgeBaseDocumentsOrchestratorTask = task({ }, maxDuration: 60 * 60, // 1 hour (in seconds) for large document batches run: async (payload: { documentIds: string[]; organizationId: string }) => { + await tags.add([`org:${payload.organizationId}`]); + logger.info('Starting Knowledge Base documents processing orchestrator', { organizationId: payload.organizationId, documentCount: payload.documentIds.length, diff --git a/apps/api/src/trigger/vendor/vendor-risk-assessment-task.ts b/apps/api/src/trigger/vendor/vendor-risk-assessment-task.ts index e47601c7fa..986c7877fa 100644 --- a/apps/api/src/trigger/vendor/vendor-risk-assessment-task.ts +++ b/apps/api/src/trigger/vendor/vendor-risk-assessment-task.ts @@ -10,7 +10,7 @@ import { import { openai } from '@ai-sdk/openai'; import type { Prisma } from '@db'; import type { Task } from '@trigger.dev/sdk'; -import { logger, metadata, queue, schemaTask } from '@trigger.dev/sdk'; +import { logger, metadata, queue, schemaTask, tags } from '@trigger.dev/sdk'; import { generateObject } from 'ai'; import { z } from 'zod'; @@ -478,6 +478,8 @@ export const vendorRiskAssessmentTask: Task< }, maxDuration: 1000 * 60 * 10, run: async (payload) => { + await tags.add([`org:${payload.organizationId}`]); + const vendor = await db.vendor.findFirst({ where: { id: payload.vendorId, diff --git a/apps/app/src/trigger/tasks/auditor/generate-auditor-content.ts b/apps/app/src/trigger/tasks/auditor/generate-auditor-content.ts index eff697bdc9..6a561b0d38 100644 --- a/apps/app/src/trigger/tasks/auditor/generate-auditor-content.ts +++ b/apps/app/src/trigger/tasks/auditor/generate-auditor-content.ts @@ -1,7 +1,7 @@ import { getOrganizationContext } from '@/trigger/tasks/onboarding/onboard-organization-helpers'; import { openai } from '@ai-sdk/openai'; import { db } from '@db/server'; -import { logger, metadata, schemaTask } from '@trigger.dev/sdk'; +import { logger, metadata, schemaTask, tags } from '@trigger.dev/sdk'; import { generateText } from 'ai'; import { z } from 'zod'; @@ -299,6 +299,7 @@ export const generateAuditorContentTask = schemaTask({ }, run: async (payload) => { const { organizationId } = payload; + await tags.add([`org:${organizationId}`]); logger.info(`Starting auditor content generation for org ${organizationId}`); diff --git a/apps/app/src/trigger/tasks/device/create-fleet-label-for-org.ts b/apps/app/src/trigger/tasks/device/create-fleet-label-for-org.ts index 68e3d3db4b..b7a278d869 100644 --- a/apps/app/src/trigger/tasks/device/create-fleet-label-for-org.ts +++ b/apps/app/src/trigger/tasks/device/create-fleet-label-for-org.ts @@ -1,7 +1,7 @@ import { getFleetInstance } from '@/lib/fleet'; import { db } from '@db/server'; -import { logger, queue, task } from '@trigger.dev/sdk'; +import { logger, queue, tags, task } from '@trigger.dev/sdk'; import { AxiosError } from 'axios'; // Optional: define a queue if we want to control concurrency in v4 const fleetQueue = queue({ name: 'create-fleet-label-for-org', concurrencyLimit: 10 }); @@ -13,6 +13,8 @@ export const createFleetLabelForOrg = task({ maxAttempts: 3, }, run: async ({ organizationId }: { organizationId: string }) => { + await tags.add([`org:${organizationId}`]); + const organization = await db.organization.findUnique({ where: { id: organizationId, diff --git a/apps/app/src/trigger/tasks/email/new-policy-email.ts b/apps/app/src/trigger/tasks/email/new-policy-email.ts index c50bf4e512..0904b07814 100644 --- a/apps/app/src/trigger/tasks/email/new-policy-email.ts +++ b/apps/app/src/trigger/tasks/email/new-policy-email.ts @@ -1,7 +1,7 @@ import { db } from '@db/server'; import { PolicyNotificationEmail } from '@trycompai/email'; import { isUserUnsubscribed } from '@trycompai/email/lib/check-unsubscribe'; -import { logger, queue, task } from '@trigger.dev/sdk'; +import { logger, queue, tags, task } from '@trigger.dev/sdk'; import { sendEmailViaApi } from '../../lib/send-email-via-api'; const policyEmailQueue = queue({ @@ -26,6 +26,7 @@ export const sendNewPolicyEmail = task({ email: payload.email, policyName: payload.policyName, }); + await tags.add([`org:${payload.organizationId}`]); try { const unsubscribed = await isUserUnsubscribed(db, payload.email, 'policyNotifications', payload.organizationId); diff --git a/apps/app/src/trigger/tasks/email/publish-all-policies-email.ts b/apps/app/src/trigger/tasks/email/publish-all-policies-email.ts index ec4dfcc624..a820199b70 100644 --- a/apps/app/src/trigger/tasks/email/publish-all-policies-email.ts +++ b/apps/app/src/trigger/tasks/email/publish-all-policies-email.ts @@ -1,7 +1,7 @@ import { db } from '@db/server'; import { AllPolicyNotificationEmail } from '@trycompai/email'; import { isUserUnsubscribed } from '@trycompai/email/lib/check-unsubscribe'; -import { logger, queue, task } from '@trigger.dev/sdk'; +import { logger, queue, tags, task } from '@trigger.dev/sdk'; import { sendEmailViaApi } from '../../lib/send-email-via-api'; const allPolicyEmailQueue = queue({ @@ -24,6 +24,7 @@ export const sendPublishAllPoliciesEmail = task({ email: payload.email, organizationName: payload.organizationName, }); + await tags.add([`org:${payload.organizationId}`]); try { const unsubscribed = await isUserUnsubscribed(db, payload.email, 'policyNotifications', payload.organizationId); diff --git a/apps/app/src/trigger/tasks/email/weekly-task-digest-email.ts b/apps/app/src/trigger/tasks/email/weekly-task-digest-email.ts index 0741cb831f..e2c16ff2fb 100644 --- a/apps/app/src/trigger/tasks/email/weekly-task-digest-email.ts +++ b/apps/app/src/trigger/tasks/email/weekly-task-digest-email.ts @@ -1,5 +1,5 @@ import { db } from '@db/server'; -import { logger, queue, task } from '@trigger.dev/sdk'; +import { logger, queue, tags, task } from '@trigger.dev/sdk'; import WeeklyTaskDigestEmail from '@trycompai/email/emails/reminders/weekly-task-digest'; import { isUserUnsubscribed } from '@trycompai/email/lib/check-unsubscribe'; import { sendEmailViaApi } from '../../lib/send-email-via-api'; @@ -34,6 +34,7 @@ export const sendWeeklyTaskDigestEmailTask = task({ organizationName: payload.organizationName, taskCount: payload.tasks.length, }); + await tags.add([`org:${payload.organizationId}`]); try { const unsubscribed = await isUserUnsubscribed(db, payload.email, 'weeklyTaskDigest', payload.organizationId); diff --git a/apps/app/src/trigger/tasks/integration/run-integration-tests.ts b/apps/app/src/trigger/tasks/integration/run-integration-tests.ts index f4dd050133..ef6754745f 100644 --- a/apps/app/src/trigger/tasks/integration/run-integration-tests.ts +++ b/apps/app/src/trigger/tasks/integration/run-integration-tests.ts @@ -1,11 +1,12 @@ import { db } from '@db/server'; -import { logger, task } from '@trigger.dev/sdk'; +import { logger, tags, task } from '@trigger.dev/sdk'; import { sendIntegrationResults } from './integration-results'; export const runIntegrationTests = task({ id: 'run-integration-tests', run: async (payload: { organizationId: string; integrationId?: string }) => { const { organizationId, integrationId } = payload; + await tags.add([`org:${organizationId}`]); logger.info( integrationId diff --git a/apps/app/src/trigger/tasks/onboarding/backfill-training-videos-for-org.ts b/apps/app/src/trigger/tasks/onboarding/backfill-training-videos-for-org.ts index 79811d588d..ae2df7ccf7 100644 --- a/apps/app/src/trigger/tasks/onboarding/backfill-training-videos-for-org.ts +++ b/apps/app/src/trigger/tasks/onboarding/backfill-training-videos-for-org.ts @@ -1,6 +1,6 @@ import { trainingVideos } from '@/lib/data/training-videos'; import { db } from '@db/server'; -import { logger, task } from '@trigger.dev/sdk'; +import { logger, tags, task } from '@trigger.dev/sdk'; export const backfillTrainingVideosForOrg = task({ id: 'backfill-training-videos-for-org', @@ -9,6 +9,7 @@ export const backfillTrainingVideosForOrg = task({ }, run: async (payload: { organizationId: string }) => { logger.info(`Starting training video backfill for organization ${payload.organizationId}`); + await tags.add([`org:${payload.organizationId}`]); try { // Get all members for this organization diff --git a/apps/app/src/trigger/tasks/onboarding/generate-full-policies.ts b/apps/app/src/trigger/tasks/onboarding/generate-full-policies.ts index b749a5e404..b4a2e5b844 100644 --- a/apps/app/src/trigger/tasks/onboarding/generate-full-policies.ts +++ b/apps/app/src/trigger/tasks/onboarding/generate-full-policies.ts @@ -1,5 +1,5 @@ import { db } from '@db/server'; -import { logger, queue, task } from '@trigger.dev/sdk'; +import { logger, queue, tags, task } from '@trigger.dev/sdk'; import { getOrganizationContext, triggerPolicyUpdates } from './onboard-organization-helpers'; // v4 queues must be declared in advance @@ -16,6 +16,7 @@ export const generateFullPolicies = task({ }, run: async (payload: { organizationId: string }) => { logger.info(`Starting full policy generation for organization ${payload.organizationId}`); + await tags.add([`org:${payload.organizationId}`]); try { // Get organization context diff --git a/apps/app/src/trigger/tasks/onboarding/generate-risk-mitigation.ts b/apps/app/src/trigger/tasks/onboarding/generate-risk-mitigation.ts index 099ef1bfee..c7d86baf65 100644 --- a/apps/app/src/trigger/tasks/onboarding/generate-risk-mitigation.ts +++ b/apps/app/src/trigger/tasks/onboarding/generate-risk-mitigation.ts @@ -1,5 +1,5 @@ import { RiskStatus, db } from '@db/server'; -import { logger, metadata, queue, task } from '@trigger.dev/sdk'; +import { logger, metadata, queue, tags, task } from '@trigger.dev/sdk'; import axios from 'axios'; import { createRiskMitigationComment, @@ -24,6 +24,7 @@ export const generateRiskMitigation = task({ policies: PolicyContext[]; }) => { const { organizationId, riskId, authorId, policies } = payload; + await tags.add([`org:${organizationId}`]); logger.info(`Generating risk mitigation for risk ${riskId} in org ${organizationId}`); const risk = await db.risk.findFirst({ where: { id: riskId, organizationId } }); @@ -85,6 +86,7 @@ export const generateRiskMitigationsForOrg = task({ queue: riskMitigationFanoutQueue, run: async (payload: { organizationId: string }) => { const { organizationId } = payload; + await tags.add([`org:${organizationId}`]); logger.info(`Fan-out risk mitigations for org ${organizationId}`); const [risks, policyRows, author] = await Promise.all([ diff --git a/apps/app/src/trigger/tasks/onboarding/generate-vendor-mitigation.ts b/apps/app/src/trigger/tasks/onboarding/generate-vendor-mitigation.ts index 8467d72124..237ab2555e 100644 --- a/apps/app/src/trigger/tasks/onboarding/generate-vendor-mitigation.ts +++ b/apps/app/src/trigger/tasks/onboarding/generate-vendor-mitigation.ts @@ -1,5 +1,5 @@ import { VendorStatus, db } from '@db/server'; -import { logger, metadata, queue, task } from '@trigger.dev/sdk'; +import { logger, metadata, queue, tags, task } from '@trigger.dev/sdk'; import axios from 'axios'; import { createVendorRiskComment, @@ -27,6 +27,7 @@ export const generateVendorMitigation = task({ policies: PolicyContext[]; }) => { const { organizationId, vendorId, authorId, policies } = payload; + await tags.add([`org:${organizationId}`]); logger.info(`Generating vendor mitigation for vendor ${vendorId} in org ${organizationId}`); const vendor = await db.vendor.findFirst({ where: { id: vendorId, organizationId } }); @@ -85,6 +86,7 @@ export const generateVendorMitigationsForOrg = task({ queue: vendorMitigationFanoutQueue, run: async (payload: { organizationId: string }) => { const { organizationId } = payload; + await tags.add([`org:${organizationId}`]); logger.info(`Fan-out vendor mitigations for org ${organizationId}`); const [vendors, policyRows, author] = await Promise.all([ diff --git a/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts b/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts index 1b1bebdb52..3927e9be2c 100644 --- a/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts +++ b/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts @@ -292,7 +292,7 @@ export async function extractVendorsFromContext( additionalProperties: false, }), system: - 'Extract vendor names from the following questions and answers. Return their name (grammar-correct), website, description, category, inherent probability, inherent impact, residual probability, and residual impact.', + 'Extract vendor names from the following questions and answers. Return their name (grammar-correct), website, description, category, inherent probability, inherent impact, residual probability, and residual impact. IMPORTANT: Always use the parent company name, not the product name (e.g. "Anthropic" not "Claude", "OpenAI" not "ChatGPT", "Alphabet" is acceptable as "Google", "Meta" is acceptable as "Meta").', prompt: questionsAndAnswers.map((q) => `${q.question}\n${q.answer}`).join('\n'), }); diff --git a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts index 5b17caeb1d..0131881fe2 100644 --- a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts +++ b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts @@ -1,5 +1,5 @@ import { db } from '@db/server'; -import { logger, metadata, queue, task, tasks } from '@trigger.dev/sdk'; +import { logger, metadata, queue, tags, task, tasks } from '@trigger.dev/sdk'; import axios from 'axios'; import { generateAuditorContentTask } from '../auditor/generate-auditor-content'; import { generateRiskMitigationsForOrg } from './generate-risk-mitigation'; @@ -23,6 +23,7 @@ export const onboardOrganization = task({ }, run: async (payload: { organizationId: string }) => { logger.info(`Start onboarding organization ${payload.organizationId}`); + await tags.add([`org:${payload.organizationId}`]); // Initialize metadata for real-time tracking metadata.set('currentStep', 'Researching Vendors...'); From 33d726b030531af63fe2533173c88848dd68acc4 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Tue, 7 Apr 2026 12:52:33 -0400 Subject: [PATCH 2/2] fix(onboarding): prevent duplicate vendors when AI renames product to company The AI now returns an original_name field alongside vendor_name so we can track both "Claude" (user input) and "Anthropic" (canonical name) in the dedup set. Without this, the fallback loop would re-add the product name as a separate vendor. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../onboard-organization-helpers.ts | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts b/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts index 3927e9be2c..2b215e2810 100644 --- a/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts +++ b/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts @@ -265,7 +265,8 @@ export async function extractVendorsFromContext( items: { type: 'object', properties: { - vendor_name: { type: 'string' }, + vendor_name: { type: 'string', description: 'The official company name (e.g. "Anthropic", not "Claude")' }, + original_name: { type: 'string', description: 'The name as it appeared in the user input (e.g. "Claude")' }, vendor_website: { type: 'string' }, vendor_description: { type: 'string' }, category: { type: 'string', enum: Object.values(VendorCategory) }, @@ -276,6 +277,7 @@ export async function extractVendorsFromContext( }, required: [ 'vendor_name', + 'original_name', 'vendor_website', 'vendor_description', 'category', @@ -292,24 +294,35 @@ export async function extractVendorsFromContext( additionalProperties: false, }), system: - 'Extract vendor names from the following questions and answers. Return their name (grammar-correct), website, description, category, inherent probability, inherent impact, residual probability, and residual impact. IMPORTANT: Always use the parent company name, not the product name (e.g. "Anthropic" not "Claude", "OpenAI" not "ChatGPT", "Alphabet" is acceptable as "Google", "Meta" is acceptable as "Meta").', + 'Extract vendor names from the following questions and answers. Return their name (grammar-correct), website, description, category, inherent probability, inherent impact, residual probability, and residual impact. IMPORTANT: For vendor_name, always use the parent company name, not the product name (e.g. "Anthropic" not "Claude", "OpenAI" not "ChatGPT"). Set original_name to the name as it appeared in the user input.', prompt: questionsAndAnswers.map((q) => `${q.question}\n${q.answer}`).join('\n'), }); - const vendors = (object as { vendors: VendorData[] }).vendors; + const rawVendors = (object as { vendors: (VendorData & { original_name?: string })[] }).vendors; - // Merge custom vendor URLs - user-provided URLs take precedence - for (const vendor of vendors) { - const customUrl = customVendorUrls.get(vendor.vendor_name.toLowerCase()); + // Strip original_name from the vendor data and build the final list + const vendors: VendorData[] = []; + const extractedVendorNames = new Set(); + + for (const { original_name, ...vendor } of rawVendors) { + vendors.push(vendor); + + // Track both the canonical name and the original user input name + extractedVendorNames.add(vendor.vendor_name.toLowerCase()); + if (original_name) { + extractedVendorNames.add(original_name.toLowerCase()); + } + + // Merge custom vendor URLs - check both canonical and original names + const customUrl = + customVendorUrls.get(vendor.vendor_name.toLowerCase()) ?? + (original_name ? customVendorUrls.get(original_name.toLowerCase()) : undefined); if (customUrl) { logger.info(`Using custom URL for vendor ${vendor.vendor_name}: ${customUrl}`); vendor.vendor_website = customUrl; } } - // Track which vendors were extracted by AI - const extractedVendorNames = new Set(vendors.map((v) => v.vendor_name.toLowerCase())); - // Ensure ALL vendors from the software field are added (not just custom ones) // This catches any vendors the AI failed to extract for (const vendorName of allVendorNames) {