Contracts

The contract module opens typed service bindings for remote APIs, workflows, and functions. Contracts support schema validation, asynchronous calls, and call-context propagation. This page is an API reference; IDs and values such as current_user represent application-owned entries and surrounding handler state.

Loading

local contract = require("contract")

Opening a Binding

Open a binding by its registry ID:

local greeter, err = contract.open("app.services:greeter")
if err then
    return nil, err
end

local result, err = greeter:say_hello("Alice")
if err then
    return nil, err
end

Bindings can also receive scope values, query parameters, or call options:

-- With scope table
local svc, err = contract.open("app.services:user", {
    tenant_id = "acme",
    region = "us-east"
})

-- With query parameters (auto-converted: "true"→bool, numbers→int/float)
local api, err = contract.open("app.services:api?debug=true&timeout=5000")

-- With call options (third argument)
local inst, err = contract.open("app.services:flaky", nil, {
    retry = { max_attempts = 5, initial_delay = 100 }
})
Parameter Type Description
binding_id string Binding ID; query parameters are supported
scope table Context values (optional, overrides query params)
options table Call options (optional) — e.g. retry.max_attempts, retry.initial_delay

Returns: Instance, error

Getting a Contract

Retrieve a contract definition for introspection:

local c, err = contract.get("app.services:greeter")
if err then
    return nil, err
end

print(c:id())  -- "app.services:greeter"

local methods = c:methods()
for _, m in ipairs(methods) do
    print(m.name, m.description)
end

local method, err = c:method("say_hello")
if err then
    return nil, err
end

Method Definition

Field Type Description
name string Method name
description string Method description
input_schemas table[] Input schema definitions (absent when the method declares none)
output_schemas table[] Output schema definitions (absent when the method declares none)

Finding Implementations

List the bindings that implement a contract:

local bindings, err = contract.find_implementations("app.services:greeter")
if err then
    return nil, err
end

for _, binding_id in ipairs(bindings) do
    print(binding_id)
end

The same lookup is available on a contract object:

local c, err = contract.get("app.services:greeter")
if err then
    return nil, err
end
local bindings, err = c:implementations()
if err then
    return nil, err
end

Checking Implementation

Check whether an already opened instance implements a contract:

if contract.is(instance, "app.services:greeter") then
    instance:say_hello("World")
end

Calling Methods

A synchronous method call blocks until it completes:

local calc, err = contract.open("app.services:calculator")
if err then
    return nil, err
end

local sum, err = calc:add(10, 20)
if err then
    return nil, err
end
local product, err = calc:multiply(5, 6)
if err then
    return nil, err
end

Async Calls

Append _async to a method name to start it asynchronously:

local processor, err = contract.open("app.services:processor")
if err then
    return nil, err
end

local future, err = processor:process_async(large_dataset)
if err then
    return nil, err
end

-- Do other work...

-- Wait for result
local ch = future:response()
local _, open = ch:receive()
if not open then
    return nil, errors.new("future response channel closed")
end

local payload, result_err = future:result()
if result_err then return nil, result_err end
local result, data_err = payload:data()
if data_err then return nil, data_err end

See Futures for future methods.

Opening via Contract

Open a binding through a contract object. The calls below are alternatives; check the error returned by contract.get() and by the selected open() call before using the instance.

local c, err = contract.get("app.services:user")
if err then
    return nil, err
end

-- Default binding
local instance, err = c:open()

-- Specific binding
local instance, err = c:open("app.services:user_impl")

-- With scope
local instance, err = c:open(nil, {user_id = 123})
local instance, err = c:open("app.services:user_impl", {user_id = 123})

Adding Context

Create a wrapper with preconfigured context values:

local ctx = require("ctx")
local c, err = contract.get("app.services:user")
if err then return nil, err end

local request_id, ctx_err = ctx.get("request_id")
if ctx_err then return nil, ctx_err end

local wrapped, err = c:with_context({
    request_id = request_id,
    user_id = current_user.id
})
if err then return nil, err end

local instance, err = wrapped:open()

Call Options

Use with_options to configure retries and other call behavior:

local c, err = contract.get("app.services:flaky")
if err then return nil, err end

local configured = c:with_options({
    retry = { max_attempts = 5, initial_delay = 100 }
})
local inst, err = configured:open("app.services:flaky_impl")
if err then return nil, err end

local result, err = inst:call()

Options apply to every method call on the returned instance. Only retryable errors trigger retries; non-retryable errors return immediately. with_options can be chained with with_context, with_actor, and with_scope.

Option Type Description
retry.max_attempts int Maximum attempts including the first (1 disables retry)
retry.initial_delay int/duration Delay before first retry (ms or duration string), default 100
retry.max_delay int/duration Upper bound for the backoff delay (ms or duration string), default 10s
retry.backoff_factor number Multiplier applied to the delay after each attempt, default 2.0
retry.jitter number Random jitter fraction applied to each delay, default 0.1
retry.retry_kinds string[] Only retry errors of these kinds; by default every kind except Invalid, PermissionDenied and Internal is retried
retry.skip_kinds string[] Never retry errors of these kinds

Security Context

Set the actor and scope used for authorization:

local security = require("security")
local c, err = contract.get("app.services:admin")
if err then return nil, err end

local secured, err = c:with_actor(security.actor())
if err then return nil, err end

secured, err = secured:with_scope(security.scope())
if err then return nil, err end

local admin, err = secured:open()
if err then return nil, err end

Without explicit with_actor/with_scope, an opened contract inherits the caller's ambient actor and scope. When set, they propagate to the bound implementation functions — every method call on the instance executes under that identity.

Permissions

Permission Resource Functions
contract.get contract id get()
contract.open binding id open(), Contract:open()
contract.implementations contract id find_implementations(), Contract:implementations()
contract.call method name sync and async method calls
contract.context "context" Contract:with_context()
contract.security "security" Contract:with_actor(), Contract:with_scope()

Errors

Condition Kind
Invalid binding ID format errors.INVALID
Contract not found errors.NOT_FOUND
Binding not found errors.NOT_FOUND
Method not found errors.NOT_FOUND
No default binding errors.NOT_FOUND
Permission denied errors.PERMISSION_DENIED
Call failed kind of the implementation's error (preserved); errors.INTERNAL for dispatch failures