Skip to content

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 function output are 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-types command, which gives typed find() / 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:

{ "type": "OrderItem" }

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]. shows sku, quantity, and price with their types.
  • Function input hints — when editing a function's expression, input. autocompletes to the fields you declared in functions[name].input
  • Function output typeoutput is available as a typed variable matching functions[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-types to get typed find() and findOne() results in your own TypeScript project based on the IDL you define here.
  • Test your IDL — use ptkl validate-idl to 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 types for the same schema blocks installation
  • An extension field key that already exists in the base component's fields for the same schema blocks installation
  • An extension function name that already exists in the component's global functions list 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.

GET /v1/system/idl

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.

POST /v1/system/idl/validate

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": true }
{
  "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:

{
  "ref": "pfn:calculate_vat",
  "value": { "amount": 100, "currency": "EUR" }
}

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
"types": {
  "Price": {
    "kind": "alias",
    "type": "number"
  },
  "Currency": {
    "kind": "enum",
    "values": ["EUR", "USD", "GBP", "RSD"]
  },
  "Money": {
    "kind": "struct",
    "fields": {
      "amount":   { "type": "Price",    "required": true },
      "currency": { "type": "Currency", "required": true }
    }
  }
}