IDL — Interface Definition Language
The IDL lets you attach a typed contract to any component's fields and functions. Once defined, the platform enforces the contract on writes and the expression editor inside the dashboard automatically provides autocomplete and type errors based on your IDL.
Overview
Without an IDL, component data/json fields accept any shape and function inputs and outputs are untyped. The IDL adds:
- Write-time validation — incoming field values are rejected if they don't match the declared shape
- Editor type safety — field types, function
input, and functionoutputare all surfaced as TypeScript types inside the dashboard's expression editor - SDK type generation — IDL field shapes are the source for the
ptkl component generate-typescommand, which gives typedfind()/findOne()results in your own codebase
Defining an IDL
The IDL is stored as a top-level idl key inside the component's settings.
Functions are global to the component — they apply regardless of the active schema.
Per-schema types and fields live inside schemas, keyed by schema name.
{
"idl": {
"version": "1.0",
"types": {},
"functions": [],
"schemas": {
"default": {
"types": {},
"fields": {}
}
}
}
}
If a component has multiple schemas (e.g., default and premium), each schema gets its own
types and fields, while types and functions at the top level are shared across all schemas:
{
"idl": {
"version": "1.0",
"types": {
"OrderItem": {
"kind": "struct",
"fields": {
"sku": { "type": "string", "required": true },
"quantity": { "type": "number", "required": true },
"price": { "type": "number", "required": true }
}
}
},
"functions": [
{
"name": "calculate_total",
"input": { "items": { "type": "array", "items": { "type": "OrderItem" } } },
"output": { "type": "object", "fields": { "total": { "type": "number" } } }
}
],
"schemas": {
"default": { "types": {}, "fields": { ... } },
"premium": { "types": {}, "fields": { ... } }
}
}
}
The top-level IDL object (ComponentIDL) has the following structure:
| Key | Description |
|---|---|
version |
Optional semver string for your own change-tracking |
types |
Global named types — available in every schema. Schema-level types with the same name take precedence. |
functions |
Global function signatures — shared across every schema of this component |
schemas |
Map of schema name → SchemaIDL (types + fields) |
Each SchemaIDL entry:
| Key | Description |
|---|---|
types |
Named type registry — reusable shapes referenced by name anywhere within the same schema |
fields |
Maps a field key to a type node — activates write-time validation for that field |
Global Types
Declare types at the top level of the component IDL under types. They are available in every
schema without repetition, so shared shapes like Money, Address, or Timestamp only need to
be defined once.
{
"idl": {
"version": "1.0",
"types": {
"Money": {
"kind": "struct",
"fields": {
"amount": { "type": "number", "required": true },
"currency": { "type": "string", "required": true }
}
}
},
"functions": [],
"schemas": {
"default": {
"types": {},
"fields": {
"price": { "type": "Money", "required": true }
}
},
"premium": {
"types": {},
"fields": {
"price": { "type": "Money", "required": true },
"gift_wrap": { "type": "boolean" }
}
}
}
}
}
Resolution priority
When the platform resolves a named type reference during validation, it uses the following priority order (highest wins):
| Priority | Source |
|---|---|
| 1 (highest) | Schema-specific types (e.g. schemas.default.types) |
| 2 | Global component types (top-level idl.types) |
| 3 | Base component types (for extension IDLs resolving via $.TypeName) |
This means a schema can shadow a global type by declaring a type with the same name, while still inheriting all other global types.
Install-time guard — an extension type name that matches a global component type blocks installation, just like a collision with a schema-specific type.
Named Types
Define shared shapes once in types — either at the global level or within a specific schema
— and reference them by name elsewhere. Types support three kinds:
| Kind | Description |
|---|---|
struct |
Object with named fields |
enum |
String literal union |
alias |
Shorthand for a primitive, array, or another type |
"types": {
"OrderItem": {
"kind": "struct",
"fields": {
"sku": { "type": "string", "required": true },
"quantity": { "type": "number", "required": true },
"price": { "type": "number", "required": true }
}
},
"OrderStatus": {
"kind": "enum",
"values": ["pending", "confirmed", "shipped", "delivered", "cancelled"]
}
}
Reference a named type anywhere a type definition is accepted by using the type name as a string:
Field Types
Add entries under fields to enforce a shape on specific component fields. The key must match the field's key in component settings.
"fields": {
"items": {
"type": "array",
"items": { "type": "OrderItem" },
"required": true
},
"shipping_address": {
"type": "object",
"fields": {
"street": { "type": "string", "required": true },
"city": { "type": "string", "required": true },
"country": { "type": "string", "required": true }
}
}
}
Once a field has an IDL entry, writes that don't match the declared shape are rejected with a structured error:
{
"error": "IDL_VALIDATION_FAILED",
"field": "items",
"path": "items[0].sku",
"expected": "string",
"received": "number"
}
Fields without an IDL entry are unaffected — they continue to accept any value.
Function Signatures
Declare function signatures at the top level of the component IDL under functions. They apply
to all schemas — you do not need to repeat them per schema. The name must match an existing
function in Settings.Functions.
"functions": [
{
"name": "calculate_total",
"input": {
"items": { "type": "array", "items": { "type": "OrderItem" }, "required": true },
"discount": { "type": "number", "required": false }
},
"output": {
"type": "object",
"fields": {
"total": { "type": "number" },
"item_count": { "type": "number" }
}
}
}
]
TypeDef Reference
Every type definition uses the same TypeDef structure:
| Property | Values | Notes |
|---|---|---|
type |
string | number | boolean | object | array | any | TypeName |
Non-primitive strings resolve against idl.types |
required |
true | false |
Only meaningful inside struct fields and function input maps |
fields |
Record<string, TypeDef> |
Only when type is "object" |
items |
TypeDef |
Only when type is "array" |
Editor Type Safety
When an IDL is present, the dashboard expression editor (Monaco) automatically provides:
- Field autocomplete — inside a component's expression scope, all IDL-declared fields are available as typed variables.
items[0].showssku,quantity, andpricewith their types. - Function input hints — when editing a function's expression,
input.autocompletes to the fields you declared infunctions[name].input - Function output type —
outputis available as a typed variable matchingfunctions[name].output, so you can dot into the expected return shape during development - Inline type errors — Monaco highlights mismatched types and missing required fields before you save
No setup is required — types are derived from the IDL as part of the component document and injected into the editor automatically.
Validation Strictness
Validation is open (lenient):
- Unknown extra fields in an object are silently accepted
- Only missing required fields and type mismatches cause a rejection
- Existing records are not retroactively validated — IDL only applies to writes after it is set
Full Example
{
"idl": {
"version": "1.0",
"types": {
"OrderItem": {
"kind": "struct",
"fields": {
"sku": { "type": "string", "required": true },
"quantity": { "type": "number", "required": true },
"price": { "type": "number", "required": true }
}
},
"Address": {
"kind": "struct",
"fields": {
"street": { "type": "string", "required": true },
"city": { "type": "string", "required": true },
"country": { "type": "string", "required": true }
}
}
},
"functions": [
{
"name": "calculate_total",
"input": {
"items": { "type": "array", "items": { "type": "OrderItem" }, "required": true },
"discount": { "type": "number", "required": false }
},
"output": {
"type": "object",
"fields": {
"total": { "type": "number" },
"item_count": { "type": "number" }
}
}
}
],
"schemas": {
"default": {
"types": {},
"fields": {
"items": {
"type": "array",
"items": { "type": "OrderItem" },
"required": true
},
"shipping": {
"type": "Address",
"required": false
}
}
}
}
}
}
Next Steps
- Typed SDK usage — use
ptkl generate-typesto get typedfind()andfindOne()results in your own TypeScript project based on the IDL you define here. - Test your IDL — use
ptkl validate-idlto validate a value against a component, extension, or platform-function IDL from the command line.
Per-Schema IDL
Components can have multiple schemas (e.g., default, premium, draft). Each schema gets its
own types and fields inside the schemas map. Functions are global — they are declared
once at the top level and apply to all schemas.
| Ref | Schema |
|---|---|
ecommerce::order |
default (implicit) |
ecommerce::order.premium |
premium |
ecommerce::order.draft |
draft |
When the platform validates a write or function call, it uses the schema from the ref to look up
the correct SchemaIDL. If no schema is specified, it defaults to "default".
Each schema's types and fields are independent — references in fields or types.fields for the
default schema can only resolve against default's types map, not premium's. Extension IDLs
can reference base component types (see Extension IDL below).
Extension IDL
Extensions can declare their own IDL alongside their fields and functions. The structure mirrors
the base component IDL — global functions and a schemas map with per-schema types and
fields.
{
"name": "premium",
"is_active": true,
"idl": {
"version": "1.0",
"functions": [
{
"name": "flag_item",
"input": { "sku": { "type": "string", "required": true } },
"output": { "type": "boolean" }
}
],
"schemas": {
"default": {
"types": {},
"fields": {
"flagged_items": {
"type": "array",
"items": { "type": "OrderItem" }
}
}
}
}
}
}
Type references in extension IDLs
Extension type names resolve using extension-scoped rules:
| Syntax | Resolves to |
|---|---|
"OrderItem" |
Extension's own types first; falls back to base component types if not found |
"$.OrderItem" |
Base component types only — $. explicitly targets the base |
"otherExtension.OrderItem" |
Rejected — cross-extension type references are not allowed |
This means an extension IDL can freely reference any type defined in the base component (by plain
name or with the $. prefix), but it cannot depend on types defined in another extension.
One-way rule
Extensions can reference base component types. Base component IDL cannot reference extension types — the base is authored without knowledge of which extensions might be installed.
Install-time guards
When an extension is installed, the platform checks its IDL against the base component's IDL for every schema the extension declares:
- An extension type name that already exists in the base component's
typesfor the same schema blocks installation - An extension field key that already exists in the base component's
fieldsfor the same schema blocks installation - An extension function name that already exists in the component's global
functionslist blocks installation
These checks guarantee type, field, and function names are always unambiguous — each name is owned by exactly one source (base or a single extension).
Validate / List Endpoint Refs
The validate endpoint and the GET /v1/system/idl response use a compound ref format to identify which resource's IDL to use. This is distinct from type name references inside the IDL itself.
| Ref format | Resolves |
|---|---|
component:{namespace}::{name} |
Base component's IDL |
component:{namespace}::{name}.{schema} |
Named schema on a base component |
extension:{namespace}::{name}/{extName} |
The extension's own IDL attached to a component |
pfn:{functionName} |
Platform function input validation (validates against signature.input) |
Example refs
component:ecommerce::order
component:ecommerce::order.draft
extension:ecommerce::order/premium
pfn:calculate_vat
Platform Function Signature
Platform functions (developer → Functions) have their own signature field that is independent of any component IDL. It carries the resolved input/output contract for callers of that function.
{
"signature": {
"input": {
"amount": { "type": "number", "required": true },
"currency": { "type": "string", "required": true },
"discount": { "type": "number", "required": false }
},
"output": {
"type": "object",
"fields": {
"net": { "type": "number" },
"vat": { "type": "number" },
"gross": { "type": "number" }
}
}
}
}
The signature can be edited in the Signature tab inside the platform function editor. When set, calls to the function have their $input validated against signature.input before the expression is evaluated. Named types are not available here — all types must be declared inline using IDLTypeNode shapes.
List All IDLs
Use GET /v1/system/idl to retrieve all IDL definitions for a project and environment in a single
call. This is what ptkl generate-types calls under the hood.
Response body
{
"components": {
"ecommerce::order": {
"types": {
"OrderItem": { "kind": "struct", "fields": { ... } }
},
"schemas": {
"default": { "types": {}, "fields": { ... } },
"premium": { "types": {}, "fields": { ... } }
},
"functions": [
{ "name": "calculate_total", "input": { ... }, "output": { ... } }
],
"extensions": {
"shipping": {
"types": {},
"schemas": {
"default": { "types": {}, "fields": { "tracking_id": { "type": "string" } } }
},
"functions": [
{ "name": "ship", "input": { ... }, "output": { ... } }
]
}
}
}
},
"functions": {
"calculate_vat": {
"input": { ... },
"output": { ... }
}
}
}
| Key | Description |
|---|---|
components |
Map of bare component ref (namespace::name) → ComponentIDLEntry. Only components with an IDL defined are included. |
components[ref].types |
Global named types shared across all schemas. |
components[ref].schemas |
Per-schema types + fields. |
components[ref].functions |
Global function signatures shared across all schemas. |
components[ref].extensions |
Active extension IDLs, each with their own types, schemas, and functions. |
functions |
Map of platform function name → signature (input/output). |
Validate Endpoint
Use POST /v1/system/idl/validate to validate a value against any IDL at runtime — useful for testing your IDL before wiring it up, or for calling from external tooling.
Request body
| Field | Type | Description |
|---|---|---|
ref |
string |
Compound ref (see Validate / List Endpoint Refs). Either ref or idl must be set. |
idl |
EntityIDL |
Inline IDL. Either ref or idl must be set. |
field |
string |
The field key to validate against (required for component / extension refs). Ignored for pfn. |
value |
any |
The value to validate. |
Response body
{
"valid": false,
"errors": [
{ "field": "items[0].sku", "message": "expected string, got number" }
]
}
HTTP status is always 200 for IDL-level results. Non-200 statuses indicate request-level failures (unknown ref, missing resource, etc.).
Examples
By ref — component field:
{
"ref": "component:ecommerce::order",
"field": "items",
"value": [{ "sku": "ABC", "quantity": 2, "price": 9.99 }]
}
By ref — platform function:
Inline IDL:
{
"idl": {
"version": "1.0",
"fields": {
"name": { "type": "string", "required": true }
}
},
"field": "name",
"value": 42
}
Named Type Kinds
IDLTypeDef entries in types must declare one of three kind values:
| Kind | Required fields | Description |
|---|---|---|
"struct" |
fields |
Object shape — validates each declared field by name |
"enum" |
values |
String literal union — value must be one of the listed strings |
"alias" |
type |
Shorthand for another type — forwards validation to the referenced type node |