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:
- A directory containing a
rank.tomlmanifest with[provider]metadata - A Node.js entry point that implements the stdio protocol
- Optionally,
.ranksource 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:
| Field | Description |
|---|---|
namespace | The name Rank programs use to import from this provider (use greeter::...) |
runtime | Must be node |
entry | Path to the Node.js entry point, relative to the manifest |
[[provider.exports]] | One block per exported function |
Each export block:
| Field | Description |
|---|---|
name | The exported function name, used in use greeter::{ Greet } |
kind | Always function for v0 |
inputSchema | Type expression for the input value. References types from the provider's source modules |
outputSchema | Type expression for the return value |
capabilities | Declared 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 requestIdis a string chosen by the compiler. Echo it back ininvokeResult- 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:
- Keep the package name and version aligned across
rank.tomlandpackage.json. - Make sure the published files include
rank.toml, your built JS files, and any shipped.rankmodules. - If you publish a scoped package to the public npm registry, set
publishConfig.access = "public". - Run
npm pack --dry-runto confirm the tarball contents beforenpm 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.tomlwith multiple exports, generic type parameters, andtypeParameterssrc/types.rankwith complex input/output schemassrc/provider.jsusingrunNodeProviderLoopfrom@rank-lang/provider-runtimefor protocol handlingsrc/runtime.jsfor 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:
| Export | Description |
|---|---|
runNodeProviderLoop({ namespace, invoke }) | Runs the full JSON-lines stdio protocol loop. Calls invoke(exportName, input) per request. |
PROVIDER_PROTOCOL_VERSION | The 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):
| Export | Description |
|---|---|
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. |