Cerial

Complex Record

Record(int), Record(uuid), and other typed record ID syntax

By default, SurrealDB generates string IDs for new records. The Record(Type) syntax lets you control the ID's value type, which flows through to all generated TypeScript types — input types, output types, and any FK fields that reference the model.

Syntax

model ModelName {
  id Record(Type) @id
  // ...
}

Replace Type with one or more supported ID types. Multiple types create a union.

Supported Types

Four primitive types are valid inside Record():

TypeSchemaTypeScript outputRequired on create?
intRecord(int) @idCerialId<number>Yes
numberRecord(number) @idCerialId<number>Yes
stringRecord(string) @idCerialId<string>No (auto-generated)
uuidRecord(uuid) @idCerialId<string>No (auto-generated)

You can also use tuple refs and object refs as ID types:

TypeSchemaTypeScript outputRequired on create?
Tuple refRecord(TupleName) @idCerialId<TupleName>Yes
Object refRecord(ObjName) @idCerialId<ObjName>Yes

A plain Record @id without a type parameter defaults to string and is optional on create.

float is not a valid ID type. Use int for integers, number for auto-detected numerics, or string/uuid for string-based IDs. Other types like bool, date, decimal, duration, bytes, geometry, and any are also invalid as ID types.

Create Optionality

Whether the id field is required or optional in the create input depends on the ID type:

DeclarationCreate id fieldWhy
Record @idid?: stringSurrealDB auto-generates string IDs
Record(string) @idid?: stringSurrealDB auto-generates string IDs
Record(uuid) @idid?: stringCerial injects rand::uuid::v7()
Record(int) @idid: numberNo auto-generation for integers
Record(number) @idid: numberNo auto-generation for numbers
Record(TupleName) @idid: TupleNameInputNo auto-generation for tuple IDs
Record(ObjName) @idid: ObjNameInputNo auto-generation for object IDs

The rule is: if string is in the type, or it's uuid as the sole type, or it's a plain Record @id, the ID is optional because SurrealDB (or Cerial) can generate one. For everything else, you must provide the ID yourself.

uuid only makes the ID optional when it's the sole type in the declaration. In a union like Record(uuid, int), the ID is required — the uuid optionality rule doesn't apply when combined with other types.

Practical Examples

Integer IDs

model Product {
  id Record(int) @id
  name String
  price Float
}
// id is required — no auto-generation for integers
const product = await client.db.Product.create({
  data: { id: 1, name: 'Widget', price: 9.99 },
});

product.id;    // CerialId<number>
product.id.id; // 1

// Query with the integer ID directly
const found = await client.db.Product.findOne({ where: { id: 1 } });

UUID IDs

model Session {
  id Record(uuid) @id
  token String
  expiresAt Date
}
// id is optional — Cerial auto-generates a UUID v7
const session = await client.db.Session.create({
  data: { token: 'abc', expiresAt: new Date() },
});

session.id;    // CerialId<string>
session.id.id; // '01942f3e-...' (auto-generated UUID string)

// Or provide your own UUID
const session2 = await client.db.Session.create({
  data: {
    id: '550e8400-e29b-41d4-a716-446655440000',
    token: 'def',
    expiresAt: new Date(),
  },
});

Union IDs

Pass multiple types separated by commas to accept more than one ID form:

model Asset {
  id Record(string, int) @id
  name String
}
// Optional: string is in the union, so SurrealDB auto-generates when omitted
const a1 = await client.db.Asset.create({ data: { name: 'Auto' } });

// Provide a string ID
const a2 = await client.db.Asset.create({ data: { id: 'logo', name: 'Logo' } });

// Provide an integer ID
const a3 = await client.db.Asset.create({ data: { id: 100, name: 'Banner' } });

a1.id; // CerialId<string | number>

Tuple and Object IDs

Structured types can be used as record IDs for composite keys:

tuple Coordinate {
  x Int,
  y Int
}

model Tile {
  id Record(Coordinate) @id
  terrain String
}
// Tuple IDs are always required
const tile = await client.db.Tile.create({
  data: { id: [10, 20], terrain: 'grass' },
});

tile.id;    // CerialId<[number, number]>
tile.id.id; // [10, 20]

Standalone Record Typing

Record(Type) can also be used on non-FK record fields — fields without a paired Relation. This is useful when you store a reference to a record but don't need Cerial's relation features (nested create, connect, disconnect):

model AuditLog {
  id Record @id
  targetId Record(int)          // typed record ref, no Relation
  action String
}
// Output
interface AuditLog {
  id: CerialId<string>;
  targetId: CerialId<number>; // typed
}

// Input — accepts the raw value or a CerialId
const log = await client.db.AuditLog.create({
  data: { targetId: 42, action: 'delete' },
});

CerialId Generic

All record ID fields produce a CerialId<T> in the output type, where T matches the declared ID type:

DeclarationOutput type.id returns
Record @idCerialId<string>string
Record(int) @idCerialId<number>number
Record(uuid) @idCerialId<string>string (UUID)
Record(Coord) @idCerialId<Coordinate>[number, number]
Record(string, int)CerialId<string|number>string | number

CerialId provides .id (the raw value), .table (the table name), .toString() for display, and .equals() for comparison. In input positions (create, where, connect), you can pass the raw value, a CerialId, a RecordId, or a StringRecordId — all are accepted via the RecordIdInput<T> union type.


See also:

  • CerialId — Full API reference for the CerialId wrapper class
  • Record Type Inference — How FK fields automatically inherit ID types from target models

On this page