twentyhq/twenty · error · Error
createOpportunity did not return an id
Error message
createOpportunity did not return an id
What it means
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 }.
Source
Thrown at packages/twenty-apps/internal/twenty-partners/src/modules/opportunity/intake/services/import-opportunity-from-tft.service.ts:113
const existing = await findOpportunityByDedupeKey(client, dedupeFilter);
const existingId = existing.opportunities?.edges?.[0]?.node?.id;
if (existingId !== undefined) return { ok: true, created: false, id: existingId };
const companyId = await findOrCreateCompanyId(client, input.company);
const pointOfContactId = await findOrCreatePersonId(
client,
input.pointOfContact,
companyId,
);
const opportunityData = mapToOpportunityCreateInput(input, {
companyId,
pointOfContactId,
});
const result = await createOpportunity(client, opportunityData);
const id = result.createOpportunity?.id;
if (id === undefined) throw new Error('createOpportunity did not return an id');
return { ok: true, created: true, id };
} catch (err) {
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
}
}
View on GitHub (pinned to 1f5dd2bbd2)
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.
Example fix
// before
const result = await createOpportunity(client, opportunityData);
const id = result.createOpportunity?.id;
if (id === undefined) throw new Error('createOpportunity did not return an id');
// after — include the input context in the surfaced reason
const result = await createOpportunity(client, opportunityData);
const id = result.createOpportunity?.id;
if (id === undefined) {
throw new Error(
`createOpportunity did not return an id (tftOpportunityId=${input.tftOpportunityId}, result=${JSON.stringify(result)})`,
);
} Defensive patterns
Strategy: try-catch
Validate before calling
// Validate the mapped opportunity input before the create.
function assertOpportunityInput(data: Record<string, unknown>) {
if (!data.stage) throw new Error('opportunityData.stage is required');
if (!data.companyId) throw new Error('opportunityData.companyId is required');
}
assertOpportunityInput(opportunityData); Type guard
const hasCreatedOpportunityId = (r: unknown): r is { createOpportunity: { id: string } } =>
typeof r === 'object' && r !== null &&
typeof (r as any).createOpportunity?.id === 'string';
if (!hasCreatedOpportunityId(result)) {
throw new Error(`createOpportunity returned no id: ${JSON.stringify(result)}`);
} Try / catch
// importOpportunityFromTft already wraps this in try/catch and returns { ok, reason }.
// Callers should branch on result.ok rather than awaiting a thrown error:
const res = await importOpportunityFromTft(input);
if (!res.ok) {
logger.warn('import failed', { reason: res.reason });
return;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- createOpportunity did not return an id
- createCompany did not return an id
- createPerson did not return an id
- createCompany did not return an id
- createPartner did not return an id
AI-assisted analysis of twentyhq/twenty@1f5dd2bbd2 (2026-08-12).
Data as JSON: /api/errors/9c975e18384ed739.
Report an issue: GitHub.