Skip to main content

Provider authoring

A Rank provider is an out-of-process Node.js program that exposes typed functions to Rank programs under a namespace. The compiler spawns the provider process, negotiates a protocol handshake, and sends typed invocation requests over stdin/stdout.

Use this guide when you want to publish your own provider package. For concrete shipped examples, compare the pure Faker Provider with the capability-bearing AWS Provider, and keep the Manifest Reference nearby for provider registration rules.

This guide walks through creating a minimal provider from scratch. The @rank-lang/plugin-faker package (packages/plugins/faker/) is the canonical reference implementation.

Overview

A provider consists of:

  1. A directory containing a rank.toml manifest with [provider] metadata
  2. A Node.js entry point that implements the stdio protocol
  3. Optionally, .rank source files that expose types and helper bindings to Rank programs

The consuming project registers the provider in its own rank.toml under [providers].

Step 1: Create the provider directory

mkdir -p providers/greeter/src
cd providers/greeter

Step 2: Write the manifest

providers/greeter/rank.toml:

manifestVersion = 1

[package]
name = "greeter-provider"
version = "0.1.0"
source = "src"

[provider]
namespace = "greeter"
runtime = "node"
entry = "./src/provider.js"

[[provider.exports]]
name = "Greet"
kind = "function"
inputSchema = "types::GreetInput"
outputSchema = "types::Greeting"
capabilities = []

Key fields:

FieldDescription
namespaceThe name Rank programs use to import from this provider (use greeter::...)
runtimeMust be node
entryPath to the Node.js entry point, relative to the manifest
[[provider.exports]]One block per exported function

Each export block:

FieldDescription
nameThe exported function name, used in use greeter::{ Greet }
kindAlways function for v0
inputSchemaType expression for the input value. References types from the provider's source modules
outputSchemaType expression for the return value
capabilitiesDeclared capabilities. Use ["network"] if the provider makes network requests. Use [] for pure providers

Step 3: Define the type schemas

providers/greeter/src/types.rank:

pub GreetInput = Object {
name: string,
formal?: bool,
}

pub Greeting = Object {
message: string,
name: string,
}

Type files are ordinary Rank source modules. They are loaded by the compiler (not the provider process) and used for type-checking the call sites.

Step 4: Implement the provider process

Add @rank-lang/provider-runtime as a dependency in your provider's package.json. This package provides runNodeProviderLoop, which handles the JSON-lines stdio protocol so you only need to implement your domain logic.

providers/greeter/package.json:

{
"name": "greeter-provider",
"version": "0.1.0",
"type": "module",
"dependencies": {
"@rank-lang/provider-runtime": "^0.1.0"
}
}

providers/greeter/src/provider.js:

import { pathToFileURL } from "node:url";
import { runNodeProviderLoop } from "@rank-lang/provider-runtime";

const NAMESPACE = "greeter";

async function invoke(exportName, input) {
switch (exportName) {
case "Greet": {
const { name, formal } = input;
const prefix = formal ? "Good day" : "Hello";
return {
ok: true,
output: {
message: `${prefix}, ${name}!`,
name,
},
};
}
default:
return {
ok: false,
code: "UnknownExport",
message: `Unknown export: ${exportName}`,
};
}
}

const invokedPath = process.argv[1];
if (invokedPath && import.meta.url === pathToFileURL(invokedPath).href) {
runNodeProviderLoop({ namespace: NAMESPACE, invoke }).catch((err) => {
process.stderr.write(`greeter provider crashed: ${err.message}\n`);
process.exitCode = 1;
});
}

runNodeProviderLoop owns the protocol handshake, version negotiation, request routing, and shutdown. Your invoke(exportName, input) function returns either { ok: true, output } or { ok: false, code, message }. See the stdio protocol section below for what the runtime is handling on your behalf.

The stdio protocol

The compiler communicates with the provider process over newline-delimited JSON on stdin/stdout. All messages are single-line JSON objects terminated by \n.

Handshake

The compiler sends:

{ "message": "initialize", "protocolVersion": 1, "namespace": "greeter" }

The provider must respond with a success result before any invocations are sent:

{ "message": "initializeResult", "protocolVersion": 1, "namespace": "greeter", "ok": true }

On failure:

{ "message": "initializeResult", "protocolVersion": 1, "namespace": "greeter", "ok": false,
"error": { "code": "UnsupportedProtocolVersion", "message": "..." } }

Invocation

{ "message": "invoke", "requestId": "req-1", "export": "Greet", "input": { "name": "Alice" } }

Success response:

{ "message": "invokeResult", "requestId": "req-1", "ok": true, "output": { "message": "Hello, Alice!", "name": "Alice" } }

Error response:

{ "message": "invokeResult", "requestId": "req-1", "ok": false,
"error": { "code": "InvalidInput", "message": "name is required" } }

Shutdown

{ "message": "shutdown" }

The provider should exit cleanly when it receives shutdown. No response is required.

Rules

  • All values exchanged are ordinary Rank-compatible data: strings, numbers, booleans, null, lists, and plain objects
  • requestId is a string chosen by the compiler. Echo it back in invokeResult
  • The provider must not write anything to stdout before receiving initialize
  • Stderr is available for diagnostic output and does not affect the protocol

Step 5: Register the provider in the project

In the consuming project's rank.toml, add a [providers] table that maps the namespace alias to the provider.

For a local provider directory:

[providers]
greeter = { path = "./providers/greeter" }

For a published npm package:

[providers]
greeter = { registry = "npm", package = "greeter-provider", version = "0.1.0" }

With a registry-backed reference, Rank fetches and caches the package on first run and writes a rank.lock file. Subsequent runs reuse the cache without network access.

The alias key must match the namespace field in the provider's rank.toml.

Step 6: Use the provider in Rank code

use greeter::{ Greet }

pub main = || Greet {
name: `Alice`,
formal: true,
}

Run:

rank src/main.rank
message: Good day, Alice!
name: Alice

Step 7: Validate

rank check src/main.rank

The compiler type-checks the call site against the schemas declared in the provider manifest. If GreetInput or Greeting do not match the actual input/output values, you will get a type error.

Publish to npm

Providers are ordinary npm packages. Publishing one is mostly standard npm packaging, with one Rank-specific requirement: the published tarball must include the provider's rank.toml, the Node.js entrypoint declared by [provider].entry, and any .rank source modules referenced from [package].source.

Before publishing:

  1. Keep the package name and version aligned across rank.toml and package.json.
  2. Make sure the published files include rank.toml, your built JS files, and any shipped .rank modules.
  3. If you publish a scoped package to the public npm registry, set publishConfig.access = "public".
  4. Run npm pack --dry-run to confirm the tarball contents before npm publish.

Example package.json additions:

{
"name": "@acme/greeter-provider",
"version": "0.1.0",
"type": "module",
"files": [
"dist",
"src",
"rank.toml"
],
"publishConfig": {
"access": "public"
},
"dependencies": {
"@rank-lang/provider-runtime": "^0.1.0"
}
}

Consumers can then install the provider directly from npm through rank.toml:

[providers]
greeter = { registry = "npm", package = "@acme/greeter-provider", version = "0.1.0" }

On the consuming side, no separate npm install step is required. Rank fetches the provider package, caches it locally, and records it in rank.lock. Providers are registry- or path-backed in this slice; git-backed references are for [dependencies], not [providers].

Provider capabilities

If your provider makes network requests, declare the capability in the manifest:

[[provider.exports]]
name = "Fetch"
kind = "function"
inputSchema = "types::FetchInput"
outputSchema = "types::FetchOutput"
capabilities = ["network"]

At the call site, pass --allow-provider-capability network:

rank src/main.rank --allow-provider-capability network

Without this flag, the compiler blocks execution and reports a capability error. This makes capability use explicit and auditable in CI pipelines.

Generic export clauses

Host exports can declare generic parameter clauses in rank.toml using the same syntax as source type aliases:

[[provider.exports]]
name = "Lookup"
kind = "function"
typeParameters = ["T", "K extends keyof T = `id`"]
inputSchema = "types::LookupInput<T, K>"
outputSchema = "T[K]"
capabilities = []

Keep one clause per string entry. Later clauses may reference earlier parameters in bounds and defaults, and defaulted clauses must remain a trailing suffix.

Reference implementation

packages/plugins/faker/ is the canonical first-party provider. Study it for:

  • rank.toml with multiple exports, generic type parameters, and typeParameters
  • src/types.rank with complex input/output schemas
  • src/provider.js using runNodeProviderLoop from @rank-lang/provider-runtime for protocol handling
  • src/runtime.js for separating domain logic from protocol mechanics

@rank-lang/provider-runtime

@rank-lang/provider-runtime is the shared JS helper package for Rank provider authors. Declare it as a runtime dependency in your provider's package.json.

Main exports:

ExportDescription
runNodeProviderLoop({ namespace, invoke })Runs the full JSON-lines stdio protocol loop. Calls invoke(exportName, input) per request.
PROVIDER_PROTOCOL_VERSIONThe current protocol version integer.
initializeResult(namespace, error?)Builds an initializeResult message. Useful for advanced protocol customization.
invokeSuccess(requestId, output)Builds a successful invokeResult message.
invokeError(requestId, code, message)Builds an error invokeResult message.

RNG utilities (from @rank-lang/provider-runtime/rand):

ExportDescription
stdlibRandSeedState(seed)Seed an RNG state from an integer.
stdlibRandDeriveState(state, label)Derive a child RNG state from a label.
stdlibRandNextUnitFloat(state)Draw the next float in [0, 1).
stdlibRandNextInt(state, min, max)Draw the next integer in [min, max] (BigInt bounds).
stdlibRandChooseIndex(state, length)Draw a random array index.