{"record":{"id":"9c975e18384ed739","repo":"twentyhq/twenty","slug":"createopportunity-did-not-return-an-id","errorCode":null,"errorMessage":"createOpportunity did not return an id","messagePattern":"createOpportunity did not return an id","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/twenty-apps/internal/twenty-partners/src/modules/opportunity/intake/services/import-opportunity-from-tft.service.ts","lineNumber":113,"sourceCode":"    const existing = await findOpportunityByDedupeKey(client, dedupeFilter);\n    const existingId = existing.opportunities?.edges?.[0]?.node?.id;\n    if (existingId !== undefined) return { ok: true, created: false, id: existingId };\n\n    const companyId = await findOrCreateCompanyId(client, input.company);\n    const pointOfContactId = await findOrCreatePersonId(\n      client,\n      input.pointOfContact,\n      companyId,\n    );\n\n    const opportunityData = mapToOpportunityCreateInput(input, {\n      companyId,\n      pointOfContactId,\n    });\n\n    const result = await createOpportunity(client, opportunityData);\n    const id = result.createOpportunity?.id;\n    if (id === undefined) throw new Error('createOpportunity did not return an id');\n\n    return { ok: true, created: true, id };\n  } catch (err) {\n    return { ok: false, reason: err instanceof Error ? err.message : String(err) };\n  }\n}\n","sourceCodeStart":95,"sourceCodeEnd":120,"githubUrl":"https://github.com/twentyhq/twenty/blob/1f5dd2bbd2a8da3419c8cfd52dd545c0024df1a6/packages/twenty-apps/internal/twenty-partners/src/modules/opportunity/intake/services/import-opportunity-from-tft.service.ts#L95-L120","documentation":"importOpportunityFromTft calls createOpportunity and asserts the returned createOpportunity.id. Like the company/person guards, it catches the case where the server returned a null opportunity payload instead of throwing. This is the final create in the TFT import pipeline, wrapped by the service's outer try/catch which converts it into { ok: false, reason }.","triggerScenarios":"createOpportunity mutation returns { createOpportunity: null } or no id. Causes: caller lacks Opportunity create permission; a required field (e.g. companyId, pointOfContactId, amount, stage) is invalid; Opportunity metadata drifted; a create-trigger returns null.","commonSituations":"Importing an opportunity whose mapped data violates a server constraint (required field, bad relation id); running under a role without Opportunity create rights; workspace metadata out of sync after a manifest update.","solutions":["Read the outer ImportOpportunityFromTftResult.reason returned to the caller — it carries this message verbatim.","Inspect the raw createOpportunity response for null payload or an errors array.","Confirm mapToOpportunityCreateInput produced valid required fields (stage, companyId, etc.).","Verify Opportunity object is installed/synced and the caller has create permission."],"exampleFix":"// before\nconst result = await createOpportunity(client, opportunityData);\nconst id = result.createOpportunity?.id;\nif (id === undefined) throw new Error('createOpportunity did not return an id');\n\n// after — include the input context in the surfaced reason\nconst result = await createOpportunity(client, opportunityData);\nconst id = result.createOpportunity?.id;\nif (id === undefined) {\n  throw new Error(\n    `createOpportunity did not return an id (tftOpportunityId=${input.tftOpportunityId}, result=${JSON.stringify(result)})`,\n  );\n}","handlingStrategy":"try-catch","validationCode":"// Validate the mapped opportunity input before the create.\nfunction assertOpportunityInput(data: Record<string, unknown>) {\n  if (!data.stage) throw new Error('opportunityData.stage is required');\n  if (!data.companyId) throw new Error('opportunityData.companyId is required');\n}\nassertOpportunityInput(opportunityData);","typeGuard":"const hasCreatedOpportunityId = (r: unknown): r is { createOpportunity: { id: string } } =>\n  typeof r === 'object' && r !== null &&\n  typeof (r as any).createOpportunity?.id === 'string';\n\nif (!hasCreatedOpportunityId(result)) {\n  throw new Error(`createOpportunity returned no id: ${JSON.stringify(result)}`);\n}","tryCatchPattern":"// importOpportunityFromTft already wraps this in try/catch and returns { ok, reason }.\n// Callers should branch on result.ok rather than awaiting a thrown error:\nconst res = await importOpportunityFromTft(input);\nif (!res.ok) {\n  logger.warn('import failed', { reason: res.reason });\n  return;\n}","preventionTips":["Treat ImportOpportunityFromTftResult as the error channel — check .ok before using .id.","Validate mapped opportunity required fields (stage, companyId) before the create.","Log the raw createOpportunity response when the id is missing.","Confirm Opportunity create permission and metadata sync."],"tags":["graphql","sdk","mutation","partners","opportunity"],"backgroundTag":null,"analyzedSha":"1f5dd2bbd2a8da3419c8cfd52dd545c0024df1a6","analyzedAt":"2026-08-12T15:37:27.593Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}