Schema: Entity, Identity, Field#
Reference for declaring a factgraph schema with Python classes: the Entity base class, the Identity and Field descriptors, the compile helpers, and the fg.schema register/extend/apply namespace.
All symbols on this page are importable from factgraph.sdk:
from factgraph.sdk import Entity, Field, Identity
from factgraph.sdk import (
compile_schema_from_classes,
schema_preflight_from_classes,
build_authoring_schema_from_classes,
)A schema is declared by subclassing Entity and assigning annotated Identity and Field descriptors. The EntityMeta metaclass reads the annotations at class-definition time and compiles them into a per-class __sdk_entity_spec__. FactGraph.create(schema_classes=[...]) consumes those specs to build the graph schema.
Entity#
Base class for Python schema entity declarations. Subclass it and declare annotated Identity and Field descriptors.
class Entity(metaclass=EntityMeta)The metaclass processes every subclass at definition time:
- It scans the class
__annotations__and matches each annotated name to an assignedIdentityorFielddescriptor. - It requires at least one
Identityfield. A subclass with noIdentityfield raisesSDKSchemaError:"Entity '<name>' must declare at least one Identity field". - It compiles the result into the class attribute
__sdk_entity_spec__, a dict with keysentity_type,identity_fields,fields, and optionallyversion,repr, andtags(the last three come from a nestedMetaclass, see below).
Annotations without an assigned Identity or Field descriptor are ignored. The class docstring is not schema metadata.
Entity.sdk_entity_spec()#
@classmethod
def sdk_entity_spec(cls) -> dict[str, Any]Returns the compiled __sdk_entity_spec__ dict.
Returns: dict[str, Any].
Raises: SDKSchemaError if the class is not a compiled Entity declaration ("class '<name>' is not a compiled Entity declaration").
Instantiation#
def __init__(self, **kwargs: Any) -> NoneEntity(**kwargs) builds a plain in-memory object. Each keyword must name a declared descriptor; an unknown attribute raises SDKSchemaError ("unknown entity attribute: <key>"). Plain instances are in-memory value holders; they are not ledger-backed. The batch-only methods .set, .add, and .retract are not available on an unset Field read from a plain instance, and calling them raises SDKSchemaError.
Example:
from factgraph.sdk import Entity, Field, Identity
class User(Entity):
user_id: str = Identity()
name: str = Field()
tags: list[str] = Field()Entity.Meta#
Optional nested class that carries entity-level metadata. Only three keys are supported: version, tags, and repr. Any other key raises SDKSchemaError: "Entity.Meta only supports version, tags, and repr; got unsupported key: <key>". Callable members and dunder members of Meta are ignored.
| Key | Type | Validation |
|---|---|---|
version | str | Must be a non-empty string, else SDKSchemaError ("Entity.Meta.version must be non-empty string"). |
tags | list[str] | Must be a list; each element must be a non-empty string, else SDKSchemaError. |
repr | str | An explain-layer template for the entity. Validated as a Meta repr template (see repr templates). |
Example:
class Person(Entity):
class Meta:
repr = "Person %name"
version = "v1"
tags = ["pii"]
name: str = Identity()
region: str = Field(repr="%ENT has region %FLD")Identity#
Declares an identity field on an Entity. Identity fields form the entity’s stable, immutable identity bundle and are used to build idref_v1 references. Every Identity() field participates in the complete identity bundle, and callers must supply the complete bundle when creating an entity.
class Identity(_DataMember):
def __init__(
self,
*,
pattern: str | None = None,
repr: str | None = None,
**legacy_kwargs: Any,
) -> NoneParameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
pattern | str | None | None | Keyword-only. Regular-expression constraint for string identity values. Must be a non-empty string and a valid regular expression. Only supported for string-typed members. |
repr | str | None | None | Keyword-only. Explain-layer representation template. Validated and compiled into Schema IR without affecting the schema digest. See repr templates. |
Cardinality: Identity fields are always single-valued. A non-single annotation (for example list[str]) raises SDKSchemaError: "Identity fields must use a single-value annotation".
Raises:
SDKSchemaErrorif any keyword other thanpatternorrepris passed. The message names the unsupported arguments and notes thatprimary_key,default, anddefault_factoryare not accepted (allIdentityfields are immutable anchor members).SDKSchemaErrorifpatternis not a non-empty string, or is not a valid regular expression.SDKSchemaErrorifpatternis set on a non-string member ("pattern= is only supported for string-typed Identity/Field members").SDKSchemaErrorifrepris not a non-empty string, or violates the member repr template rules.
Example:
class TenantUser(Entity):
user_id: str = Identity()
tenant_id: str = Identity()
name: str = Field()Reading the descriptor from the class (for example User.user_id) returns the Identity descriptor itself, which is used as a navigation key by the fg.fields.* and fg.assertions.* APIs.
Field#
Declares a non-identity field on an Entity.
class Field(_DataMember):
def __init__(
self,
*,
pattern: str | None = None,
repr: str | None = None,
**legacy_kwargs: Any,
) -> NoneParameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
pattern | str | None | None | Keyword-only. Regular-expression constraint for string fields. Must be a non-empty string and a valid regular expression. Only supported for string-typed fields. |
repr | str | None | None | Keyword-only. Explain-layer representation template. Validated and compiled into Schema IR without affecting the schema digest. See repr templates. |
Cardinality: inferred from the type annotation, not passed as an argument. Scalar annotations create single-value fields; list[T], set[T], frozenset[T], and variadic tuple[T, ...] annotations create multi-value fields. See Cardinality. The Field(cardinality=...) keyword is removed; passing it raises SDKSchemaError with a message directing you to use a scalar annotation (single) or a collection annotation (multi).
Defaults and backfill: fields do not support defaults or backfill. A missing single field reads as None; a missing multi field reads as ().
Raises:
SDKSchemaErrorif any keyword other thanpatternorrepris passed (the message documents thecardinality=removal).SDKSchemaErrorifpatternis not a non-empty string, is not a valid regular expression, or is set on a non-string field.SDKSchemaErrorifrepris not a non-empty string, or violates the member repr template rules.
Field.cardinality#
@property
def cardinality(self) -> strReturns the inferred cardinality, "single" or "multi". Available only after the descriptor is bound to a schema class. Reading it before binding raises SDKSchemaError: "Field cardinality is unavailable until the descriptor is bound to a schema class".
Example:
class Person(Entity):
name: str = Identity()
region: str = Field() # single
tags: list[str] = Field() # multi
age: int = Field() # single, int-typed
Person.region.cardinality # "single"
Person.tags.cardinality # "multi"Cardinality#
Cardinality is determined entirely by the annotation, evaluated against both real type objects and string (forward-reference) annotations.
| Annotation form | Cardinality |
|---|---|
scalar, for example str, int, bool | single |
list[T], set[T], frozenset[T] (also List, Set, FrozenSet) | multi |
tuple[T, ...] (variadic, also Tuple[T, ...]) | multi |
Literal[...] | single (enum) |
Constraints:
- A collection annotation must specify exactly one element type, else
SDKSchemaError("multi-cardinality field annotation must specify exactly one element type"). - The element type of a collection must itself be scalar (single), else
SDKSchemaError("multi-cardinality fields must use a scalar element annotation"). - A non-variadic
tuple[...]is rejected:SDKSchemaError("tuple fields must use tuple[T, ...] for multi-cardinality Form I fields"). Optional[...]andUnion[...]annotations (includingX | Y) are rejected:SDKSchemaError("Optional/Union annotations are not supported in Form I schema declarations").dict[...]annotations are rejected:SDKSchemaError("dict annotations are not supported in Form I schema declarations").- Any other parameterized generic is rejected:
SDKSchemaError("unsupported generic annotation in Form I schema declarations").
Type domains#
Each annotation maps to a canonical type domain. Built-in scalars map as follows:
| Python annotation | Type domain |
|---|---|
str | string |
int | int |
bool | bool |
bytes | bytes |
float | float64 |
uuid.UUID | uuid |
datetime.datetime | time |
Entity subclass | entity_ref |
An annotation that is an Entity subclass (or an unrecognized type, or an unrecognized name in a string annotation) maps to the entity_ref domain. The canonical domain names (entity_ref, string, int, float64, bool, bytes, time, uuid) are also accepted directly when written as string annotations.
Enum fields with Literal#
A Literal[...] annotation declares an enum field. The compiler computes a single canonical type domain from the literal values:
boolvalue tobool,strtostring,inttoint,floattofloat64,bytestobytes,uuid.UUIDtouuid,datetime.datetimetotime.- All literal values must resolve to the same canonical type domain, else
SDKSchemaError("Literal[...] enum values must all use the same canonical type"). Literal[...]must declare at least one value, elseSDKSchemaError("Literal[...] enum fields must declare at least one value").- Float-typed literals are rejected:
SDKSchemaError("Literal[...] float enum values are not supported; use an unconstrained float field"). - A literal value of an unsupported type raises
SDKSchemaError("Literal[...] enum value has unsupported type: <type>").
The declared values are carried into the authoring spec as enum_values.
class Ticket(Entity):
ticket_id: str = Identity()
status: Literal["open", "closed"] = Field()repr templates#
repr templates supply explain-layer text. They are validated at class-definition time and compiled into the Schema IR without affecting the schema digest. Template syntax uses %-prefixed placeholders matching %[A-Za-z_][A-Za-z0-9_]*. A % not followed by a letter or underscore is a malformed placeholder and raises an error.
Member templates (Identity(repr=...), Field(repr=...)) may reference only these reserved placeholders:
| Placeholder | Meaning |
|---|---|
%CLS | the class |
%ENT | the entity |
%FLD | the current field value |
A member template referencing any other placeholder (for example a sibling field name) raises SDKSchemaError ("member repr references unsupported placeholder %<name>; allowed placeholders are %CLS, %ENT, and %FLD").
Meta templates (Entity.Meta.repr) may reference %CLS and any of the entity’s identity-field names. They may not reference %ENT or %FLD (SDKSchemaError: "Meta.repr cannot reference %ENT" / %FLD), and may not reference a non-identity field ("Meta.repr references non-identity placeholder %<name>").
An identity field whose name collides with a reserved placeholder (CLS, ENT, FLD) is rejected when a Meta repr template is used.
Example:
class Person(Entity):
class Meta:
repr = "Person %name" # %CLS + identity field name
name: str = Identity(repr="%CLS %ENT %FLD")
region: str = Field(repr="%ENT has region %FLD")compile_schema_from_classes(classes, *, generated_at=None) -> dict[str, Any]#
Compiles SDK schema classes into canonical schema IR, the exact schema dictionary that backs a FactGraph. Normal graph construction calls it for you.
def compile_schema_from_classes(
classes: list[type[Any]],
*,
generated_at: str | None = None,
) -> dict[str, Any]Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
classes | list[type[Any]] | required | Non-empty list of Entity subclasses. |
generated_at | str | None | None | Keyword-only timestamp override for deterministic tests. |
Returns: dict[str, Any], the canonical schema IR.
Raises: SDKSchemaError if classes is not a non-empty list, or if any element is not an Entity subclass.
Example:
from factgraph.sdk import compile_schema_from_classes
schema_ir = compile_schema_from_classes([Person])schema_preflight_from_classes(classes, *, generated_at=None) -> dict[str, Any]#
Validates SDK schema classes without creating a graph and returns the authoring preflight report produced by the schema compiler.
def schema_preflight_from_classes(
classes: list[type[Any]],
*,
generated_at: str | None = None,
) -> dict[str, Any]Parameters: same as compile_schema_from_classes.
Returns: dict[str, Any], the schema preflight report.
Raises: SDKSchemaError if classes is not a non-empty list, or if any element is not an Entity subclass.
build_authoring_schema_from_classes(classes) -> dict[str, Any]#
Lower-level bridge that builds authoring-schema input from SDK schema classes, used before schema compilation. For normal use, pass schema_classes=[...] to FactGraph.create(...) instead.
def build_authoring_schema_from_classes(
classes: list[type[Any]],
) -> dict[str, Any]Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
classes | list[type[Any]] | required | Non-empty list of Entity subclasses. |
Returns: dict[str, Any], an authoring-schema payload with key entities (a list of per-entity authoring specs).
Raises: SDKSchemaError if classes is not a non-empty list, or if any element is not an Entity subclass.
Example:
authoring = build_authoring_schema_from_classes([User])
authoring["entities"][0]["entity_type"] # "User"fg.schema#
Read-only namespace on a FactGraph for additive schema mutation. The namespace itself cannot be reassigned (FrozenSnapshotError). All three mutating methods are unavailable on attached runtimes; calling them there raises SDKStoreError.
Each method returns a SchemaAddResult (frozen dataclass) with these fields:
| Field | Type | Description |
|---|---|---|
old_digest | str | Schema digest before the transition. |
new_digest | str | Schema digest after the transition. |
added_entities | list[str] | Newly added entity types. |
added_fields | list[str] | Newly added non-identity fields, as "<Entity>.<field>" strings. |
A no-op call (one whose compiled schema digest is unchanged with no added entities or fields) returns a SchemaAddResult whose old_digest equals new_digest and whose added_entities and added_fields are empty.
fg.schema.register(entity_cls) -> SchemaAddResult#
def register(self, entity_cls: type[Entity]) -> SchemaAddResultRegisters a new Entity type.
Parameters: entity_cls (type[Entity]), the entity class to register.
Returns: SchemaAddResult whose added_entities lists the new entity type.
Raises:
SchemaConflictError(codeSCHEMA_CONFLICT) if the entity type is already registered ("entity_type already registered: <name>").SDKStoreErrorif called on an attached runtime.
Example:
result = fg.schema.register(Account)
result.added_entities # ["Account"]fg.schema.extend(entity_cls) -> SchemaAddResult#
def extend(self, entity_cls: type[Entity]) -> SchemaAddResultAdds non-identity Fields to an already-registered Entity type. The supplied class must keep the existing identity bundle and existing fields unchanged; only new non-identity fields may be added.
Parameters: entity_cls (type[Entity]), the redeclared entity class carrying the additional fields.
Returns: SchemaAddResult whose added_fields lists the new fields as "<Entity>.<field>".
Raises:
SchemaNotFoundError(codeSCHEMA_NOT_FOUND) if the entity type is not registered ("entity_type not registered: <name>").SchemaNonAdditiveError(codeSCHEMA_NON_ADDITIVE) for any non-additive change: adding or demoting/promoting an identity field, removing a field, changing a field’s cardinality, or changing a field’s type. The schema state is left unchanged when the call is rejected.SDKStoreErrorif called on an attached runtime.
Example:
class User(Entity):
user_id: str = Identity()
name: str = Field()
tags: list[str] = Field()
nickname: str = Field() # new field
result = fg.schema.extend(User)
result.added_fields # ["User.nickname"]fg.schema.apply(entity_cls) -> SchemaAddResult#
def apply(self, entity_cls: type[Entity]) -> SchemaAddResultRegisters a new entity or extends an existing one. If the entity type is already registered, it routes to extend; otherwise it routes to register.
Parameters: entity_cls (type[Entity]), the entity class to register or extend.
Returns: SchemaAddResult from the chosen route.
Raises: the same errors as register or extend, depending on the route taken.
Related pages#
- Three-layer API: the
fg.entities,fg.fields, andfg.assertionsnamespaces that consumeIdentity/Fielddescriptors as navigation keys. - factgraph:
FactGraph.create,from_schema_classes, andload_workspaceconstructors. - Errors:
SDKSchemaError,SDKStoreError,SchemaConflictError,SchemaNotFoundError,SchemaNonAdditiveError, andFrozenSnapshotError.