twentyhq/twenty · error · Error

createPerson did not return an id

Error message

createPerson did not return an id

What it means

findOrCreatePersonId in import-opportunity-from-tft guards the createPerson mutation result the same way as the company guard. After selecting createPerson.id, an undefined id means the server returned a null person payload rather than throwing. It exists because a Person insert can be silently rejected (e.g. email uniqueness handled server-side, or field validation) while the transport still reports success.

Source

Thrown at packages/twenty-apps/internal/twenty-partners/src/modules/opportunity/intake/services/import-opportunity-from-tft.service.ts:75

  const lastName = isNonEmptyString(pointOfContact?.lastName)
    ? pointOfContact.lastName.trim()
    : '';
  if (email === undefined && firstName === '' && lastName === '') return undefined;

  if (email !== undefined) {
    const existing = await findPersonIdByPrimaryEmail(client, email);
    if (existing !== undefined) return existing;
  }

  const personData: CoreSchema.PersonCreateInput = { name: { firstName, lastName } };
  if (email !== undefined) personData.emails = { primaryEmail: email };
  if (companyId !== undefined) personData.companyId = companyId;

  const result = await client.mutation({
    createPerson: { __args: { data: personData }, id: true },
  });
  const id = result.createPerson?.id;
  if (id === undefined) throw new Error('createPerson did not return an id');
  return id;
}

// Manual one-way copy of one Opportunity from the TFT workspace into partners; idempotent on
// tftOpportunityId (name as a fallback for manual calls).
export async function importOpportunityFromTft(
  input: ImportOpportunityFromTftInput,
): Promise<ImportOpportunityFromTftResult> {
  try {
    const client = new CoreApiClient();
    const name = input.name.trim();
    const tftOpportunityId = isNonEmptyString(input.tftOpportunityId)
      ? input.tftOpportunityId.trim()
      : undefined;

    const dedupeFilter: CoreSchema.OpportunityFilterInput =
      tftOpportunityId !== undefined
        ? { tftOpportunityId: { eq: tftOpportunityId } }

View on GitHub (pinned to 1f5dd2bbd2)

Solutions

  1. Log the full result object to see whether createPerson is null or whether an errors array accompanied it.
  2. Confirm the caller has Person create permission and that the Person object/fields are synced in metadata.
  3. Validate firstName/lastName/email are non-empty and the email is well-formed before calling createPerson.
  4. Reproduce createPerson in the GraphQL playground against the same workspace to read the server's rejection message.

Example fix

// before
const result = await client.mutation({
  createPerson: { __args: { data: personData }, id: true },
});
const id = result.createPerson?.id;
if (id === undefined) throw new Error('createPerson did not return an id');

// after — guard input + richer error
const result = await client.mutation({
  createPerson: { __args: { data: personData }, id: true },
});
const id = result.createPerson?.id;
if (id === undefined) {
  throw new Error(
    `createPerson did not return an id for email=${email} (result=${JSON.stringify(result)})`,
  );
}
Defensive patterns

Strategy: type-guard

Validate before calling

import { isNonEmptyString } from 'twenty-shared';

function assertPersonInput(input: { name?: unknown; emails?: unknown }) {
  // name is required server-side; email strongly recommended for dedup
  if (!input.name || typeof input.name !== 'object') {
    throw new Error('createPerson requires a name object');
  }
}

Type guard

const hasCreatedPersonId = (r: unknown): r is { createPerson: { id: string } } =>
  typeof r === 'object' && r !== null &&
  typeof (r as any).createPerson?.id === 'string';

if (!hasCreatedPersonId(result)) {
  throw new Error(`createPerson returned no id: ${JSON.stringify(result)}`);
}

Try / catch

try {
  const id = await findOrCreatePersonId(client, input.pointOfContact, companyId);
  // use id
} catch (err) {
  return { ok: false, reason: err instanceof Error ? err.message : String(err) };
}

Prevention

When it happens

Trigger: createPerson mutation returns { createPerson: null } or an object lacking id. Happens when the Person object metadata is stale, the caller lacks Person create permission, the supplied email/name fails server validation, or a create-trigger returns null.

Common situations: Importing a TFT contact whose email is malformed or already mapped; running the import under a role without Person create rights; workspace not re-synced after a Person field change; partial GraphQL response with errors swallowed by the SDK.

Related errors


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