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

  1. Read the outer ImportOpportunityFromTftResult.reason returned to the caller — it carries this message verbatim.
  2. Inspect the raw createOpportunity response for null payload or an errors array.
  3. Confirm mapToOpportunityCreateInput produced valid required fields (stage, companyId, etc.).
  4. 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

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


AI-assisted analysis of twentyhq/twenty@1f5dd2bbd2 (2026-08-12). Data as JSON: /api/errors/9c975e18384ed739. Report an issue: GitHub.