Code mode for agents.
Turn any API or host capability into a callable action. Install a plugin, write JavaScript, call actions. Code runs in a QuickJS WASM runtime with plugin globals; enable the built-in node plugin when you want filesystem, path, OS, process, crypto, shell, and fetch actions.
npm install -g runlinenpm install -g runline
runline init
runline connection add bf --plugin brandfetch --set apiKey=xxx
runline exec 'return await brandfetch.brand.getColors({ domain: "nike.com" })'
# => [{ hex: "#E5E5E5", type: "accent" }, { hex: "#111111", type: "dark" }, ...]All 204 built-in plugins ship bundled inside runline — no per-plugin install step. Just add a connection for the one you want to use. Agent code runs in a QuickJS runtime: each configured plugin is a top-level global, dot-chain into resource and action. Configure the built-in node plugin for host work like node.fs.readFile, node.process.execFile, node.path.join, node.crypto.hash, and node.fetch.
// agent writes this
const company = await brandfetch.brand.getCompany({ domain: "stripe.com" });
const deals = await pipedrive.deal.list({ limit: 10 });
const issue = await github.issue.create({
owner: "acme", repo: "api",
title: `New lead: ${company.name}`,
body: `${deals.length} open deals`
});
return { company: company.name, issue: issue.number };204 built-in plugins covering popular SaaS, DevOps, productivity, image-generation APIs, and host Node capabilities. All ship with the package — no separate install needed.
Set the env var shown in the Auth column, add a connection, and go:
export GITHUB_TOKEN=ghp_xxx
runline connection add gh --plugin github --set token=$GITHUB_TOKEN
runline exec 'return await github.user.listRepos({ username: "torvalds" })'# List all available actions
runline actions
# List actions only for connected services
runline actions --connected
# Get Nike's brand colors
runline exec 'return await brandfetch.brand.getColors({ domain: "nike.com" })'
# Create a GitHub issue
runline exec '
return await github.issue.create({
owner: "acme", repo: "api",
title: "Bug: login broken",
labels: ["bug", "urgent"]
})
'
# Search Pipedrive deals
runline exec 'return await pipedrive.deal.search({ term: "Acme" })'
# Chain actions together
runline exec '
const contact = await hubspot.contact.get({ id: "123" });
const task = await todoist.task.create({
content: `Follow up with ${contact.properties.firstname}`,
priority: 4
});
return { contact: contact.properties.email, taskId: task.id };
'
# Discover actions from inside the runtime
runline exec 'return actions.list()'
runline exec 'return actions.list("brandfetch")'
runline exec 'return actions.find("create issue")'
runline exec 'return actions.describe("github.issue.create")'
runline exec 'return actions.check("github.issue.create", { owner: "a" })'
# Output as JSON (for agents)
runline exec 'return await github.repo.list({ owner: "torvalds" })' --jsonPlugins export a function that receives a RunlinePluginAPI and registers actions. Use TypeBox schemas for both connection config and action input; Runline uses them for action discovery, actions.check(...), and validation before execute runs.
A plugin that calls an authenticated API declares its credential instead of building auth headers itself. The declaration says what the secret is, how it is placed, and the exact HTTPS targets it may be sent to; every request goes through credentialJson / credentialRequest, which sign it — through the host's broker when there is one, else locally from the connection config. The plugin never touches the secret, and nothing outside its declared targets can receive it.
import {
credentialJson,
pathSegment,
type RunlinePluginAPI,
staticCredential,
} from "runline";
import * as t from "typebox";
// Bearer token from the connection's `apiKey`, sent only beneath this base.
const credential = staticCredential({
id: "orders",
auth: { kind: "bearer" },
local: { secret: "apiKey" },
targets: {
api: { baseUrl: "https://api.orders.example/v1/", methods: ["GET", "POST"] },
},
probe: { target: "api", path: "me", method: "GET", acceptedStatuses: [200] },
});
export default function orders(rl: RunlinePluginAPI) {
rl.setName("orders");
rl.setVersion("1.0.0");
rl.setCredential(credential);
rl.setConnectionSchema(
t.Object({ apiKey: t.String({ env: "ORDERS_API_KEY" }) }),
);
rl.registerAction("list", {
access: "read",
description: "List an organization's orders, newest first.",
inputSchema: t.Object(
{
orgId: t.String({ minLength: 1, description: "Organization ID" }),
status: t.Optional(t.Union([t.Literal("open"), t.Literal("closed")])),
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
},
{ additionalProperties: false },
),
async execute(input, ctx) {
const { orgId, ...query } = input as { orgId: string };
return credentialJson(ctx, credential, "orders", {
target: "api",
path: `orgs/${pathSegment(orgId)}/orders`,
query,
});
},
});
rl.registerAction("create", {
access: "write",
description: "Create an order. Returns the created order.",
inputSchema: t.Object(
{
orgId: t.String({ minLength: 1 }),
customer: t.String({ minLength: 1 }),
total: t.Number({ minimum: 0 }),
},
{ additionalProperties: false },
),
async execute(input, ctx) {
return credentialJson(ctx, credential, "orders", {
target: "api",
path: "orders",
method: "POST",
json: input,
});
},
});
}The credential kit is exported from runline for every plugin, bundled or custom:
| Helper | Use |
|---|---|
staticCredential(spec) |
Declare a static secret: bearer, apiKey (named header, optional prefix), queryKey, basic, or the general form of named parts and placements. local names the flat config fields it signs from; targets may be a function of public config. |
grantSchema |
The stored shape of an OAuth method's revisioned grant, for plugins declaring oauth2. |
credentialJson / credentialOk / credentialRequest |
Send one request through the declared credential: JSON answer, successful Response, or raw Response. query, json, form, body, headers and WebDAV destination are built for you. |
credentialSocketUrl |
A signed (or host-relayed) URL for a declared wss:// target. |
brokerFor(ctx, declaration) |
The broker one action call signs through, for requests and probes the helpers above do not shape: the host's, or the local signer. |
pathSegment / pathSegments / slashEncodedSegment |
Put caller input into a path as exactly one segment (or slash-separated segments); .., empty segments and separators are refused. |
pathWithin |
Turn an API-returned next-page URL back into a path beneath the declared target, refusing anything outside it. |
httpsBase / hostLabel / configChoice / configFlag |
Derive a target from public config, refusing anything that could send the credential to another host. |
responseFailed / requestFailed / failureMessage / errorIdentifier |
Report a failed response by status and identifier-shaped codes only: provider free text, where a secret may be echoed, never reaches the error. |
answerFailed / graphqlFailed |
Report a failure inside a 2xx answer (ok: false, GraphQL errors) by code, field and a one-line message. |
jsonAnswer / jsonOrAcknowledged |
Read a successful answer; an empty or 204 answer is { success: true }. |
multipartBody |
Buffer FormData into the bytes and boundary the transport accepts. |
refuseUnderHost |
Refuse an owner-login step when a host keeps the credential. |
awsCredential(spec) / awsTarget(service, region) |
Declare an AWS connection: an access key (accessKeyId, secretAccessKey, a session's sessionToken) or, signing locally, an AWS CLI profile (awsProfile, e.g. an SSO login) in its place; with roleArn, a role that key assumes, signing with the role's sessions. Targets are regional service endpoints, each signed for its service and region. |
awsJson / awsQuery |
Call an AWS JSON-protocol service by operation (AmazonSSM.SendCommand), or a Query-protocol service by action (DescribeInstances), its XML answer read as data. |
awsFailed / awsList / parseAwsXml / awsEndpoint / awsRegion |
Report a failed AWS call by status and AWS error code; read an XML list's item or member entries; parse AWS XML; build and validate endpoints and regions. |
An AWS plugin declares its credential the same way, and signs through SigV4:
import {
awsCredential,
awsJson,
awsRegion,
awsTarget,
type RunlinePluginAPI,
} from "runline";
import * as t from "typebox";
// An access key, or with `roleArn` in the connection a role it assumes.
const credential = awsCredential({
id: "aws",
targets: (config) => ({
ssm: awsTarget("ssm", awsRegion(config.region ?? "us-east-1")),
}),
});
export default function fleet(rl: RunlinePluginAPI) {
rl.setName("fleet");
rl.setCredential(credential);
rl.registerAction("instances.list", {
access: "read",
description: "Instances managed by Systems Manager.",
inputSchema: t.Object({}, { additionalProperties: false }),
execute: (_input, ctx) =>
awsJson(ctx, credential, "fleet", {
target: "ssm",
operation: "AmazonSSM.DescribeInstanceInformation",
version: "1.1",
}),
});
}See Host-signed requests for the declaration model itself: authentication kinds, placements and targets.
Custom plugins are discovered from the nearest project .runline/ directory first, then .runline/plugins.json, then ~/.runline/plugins, then the bundled built-ins named by configured connections. Project-local plugins win over same-named global or built-in plugins; duplicates discovered later are skipped and reported on stderr.
Supported local layouts:
.runline/plugins/orders.ts
.runline/plugins/orders.js
.runline/plugins/orders/index.ts
.runline/plugins/orders/index.js
.runline/plugins/orders/src/index.ts
.runline/plugins/orders/src/index.js
.runline/plugins/orders/package.json # { "main": "dist/index.js" }
A package directory can also expose multiple plugins:
{
"runline": { "plugins": ["./orders.js", "./billing.js"] }
}plugins.json can also point at explicit plugin paths:
{
"plugins": [{ "path": "./tools/orders.ts" }]
}A plugin may export the default registration function shown above, or a plain plugin definition object with { name, version, actions }. ESM, TypeScript, and CommonJS object exports are supported. The function form is preferred because it validates plugin names, keeps action registration consistent, and gives authors the public RunlinePluginAPI surface.
Actions can include optional access: "read" | "write" metadata. Use read for actions that only retrieve data, and write for anything that creates, updates, deletes, sends, uploads, executes, triggers, deploys, imports, changes config, writes local files, or may otherwise affect an external system. Omit it only for legacy compatibility; built-in plugins include it.
Plugin execute handlers run outside the QuickJS runtime with full Node.js access (fetch, fs, npm dependencies available to the host process, etc). Agent code calls them through configured plugin globals. ctx.connection.config contains the resolved connection config with env var overrides applied, and ctx.updateConnection(patch) can persist refreshed credentials. Add a node connection only when you explicitly want to expose host APIs through the built-in node plugin.
Agents can inspect custom plugins from inside runline exec before calling them:
runline exec 'return actions.list("orders")'
runline exec 'return actions.find("list orders")'
runline exec 'return actions.describe("orders.list")'
runline exec 'return actions.check("orders.list", { orgId: "acme" })'
runline exec 'return await orders.list({ orgId: "acme", status: "open" })'For debugging, run runline actions --json to see loaded actions, access metadata, and schemas. Loader failures include the plugin path and reason on stderr. A broken plugin does not stop discovery of healthy plugins in the same directory; common failures include invalid exports, invalid plugin names, malformed package.json/plugins.json, missing entrypoints, and duplicate plugin names.
See packages/runline-plugins/ for 211 real-world examples.
Agent code runs in a QuickJS WASM runtime:
- Timeout — configurable, kills infinite loops
- Memory limit — configurable, prevents OOM
console.log— captured and returned inresult.logs- Plugin globals — each installed plugin is a top-level proxy (e.g.
github,slack,brandfetch). Dot-chain into resource and action:github.issue.create(input) - Configured Node globals — when configured, the built-in
nodeplugin exposes host-backed actions such asnode.fs.readFile,node.fs.writeFile,node.path.join,node.os.info,node.process.execFile,node.crypto.hash, andnode.fetch
Every command supports --json. Use runline actions --json for full schemas with input types.
runline actions --json # all actions with schemas
runline actions --connected --json # only connected services
runline exec '<code>' --json # structured { result, logs } outputimport { Runline } from "runline";
import brandfetch from "runline-plugin-brandfetch";
const rl = Runline.create({
plugins: [brandfetch],
connections: [{ name: "bf", plugin: "brandfetch", config: { apiKey: "xxx" } }],
});
const result = await rl.execute(`
const colors = await brandfetch.brand.getColors({ domain: "stripe.com" });
return colors.filter(c => c.type === "accent");
`);
console.log(result.result); // [{ hex: "#635BFF", type: "accent", brightness: 116 }]A host that keeps credentials outside the process running actions — a server signing for a sandboxed worker — passes credentialBroker. The engine calls it once per action call of a plugin that declares its credential (setCredential), with { plugin, action, context } (context is whatever that execute() passed) and hands the result to the action as ctx.credentials. Every built-in that declares its credential then sends every request and probe through it and signs nothing itself, so its connection config carries public settings only.
const rl = Runline.create({
plugins: [gmail],
credentialBroker: ({ plugin, action, context }) => ({
request: (req) => signOnServer({ plugin, action, context, req }),
probe: () => probeOnServer({ plugin, context }),
}),
});The broker is the authority: authorize the call from context and action before signing, because plugin code chooses the request. Sign with what the plugin declares — plugin.credential(config) on the definition the host loaded itself gives the credential type (allowed targets, token endpoints, scopes), method, application, JWT identity and AWS role — never with anything the plugin's process sends.
Each method's authentication.field names the one config field a host stores the secret in. There are four kinds:
| kind | stored shape | sent as |
|---|---|---|
none |
nothing | unsigned, still held to the method's targets: the method of a connection without an optional credential |
static |
its named parts, each a string, optionalParts allowed to be absent (staticSecretSchema(parts, optionalParts)) |
each part through its placements |
oauth2 |
a revisioned OAuthGrant |
Authorization: Bearer <access token>, renewed once on rejection by its declared renewal: a refresh token, client credentials, a JWT bearer assertion (jwtIdentity), or the resource owner's username and password (resourceOwner, under the endpoint's own fields names) |
awsSigV4 |
an AwsAccessKeySchema key: accessKeyId, secretAccessKey, and a session's sessionToken |
AWS Signature Version 4 for each target's declared aws: { service, region }. With assumeRole: { sessionField }, the key only assumes the binding's awsRole through STS; requests sign with the role's session (AwsSessionSchema, in sessionField), renewed five minutes before it expires |
A static placement is one of:
| placement | sent as |
|---|---|
{ in: "header", part, name, prefix? } |
<name>: <prefix><part> |
{ in: "query", part, name } |
?<name>=<part> |
{ in: "body", part, name } |
a top-level field of a JSON-object or form body; ?<name>=<part> when the request has no body |
{ in: "jsonPointer", part, pointer } |
the value at an RFC 6901 pointer in a JSON body (/params/args/2), where the request carries null for the transport to fill |
{ in: "path", part, prefix? } |
the first path segment beneath the base, <prefix><part>; at most one per method. A secret in a URL can reach access logs, so a host may refuse this placement |
{ in: "basic", username, password } |
Authorization: Basic base64(username:password); either part may be empty |
{ in: "jwt", part, name, prefix?, audience } |
<name>: <prefix><JWT>: an HS256 JWT signed per request from a <key id>:<hex secret> part, for audience, valid five minutes |
{ in: "querySignature", part, name } |
<name>: base64(HMAC-SHA256(query)), keyed by the part, over the query as sent, after every other placement |
Every header, query parameter and body field a placement sets is reserved: a caller may never supply it. A placement may name targets it alone signs, so a connection holding two credentials for two APIs keeps them as two parts scoped to their own targets; every target of a static method must be signed by at least one placement. A part listed in optionalParts may be missing, which refuses only the targets that place it. staticCredential declares the common single-secret cases by shorthand, which also names the method: bearer ({ secret } in Authorization: Bearer), apiKey ({ secret } in a named header with an optional prefix), queryKey ({ secret } as a query parameter) and basic ({ username, password }). With optional: true the type also declares a none method, which a connection selects unless it holds the flat secret fields or says authenticated: true in its public config.
A public config field that picks a host — a region, an environment, a hosting mode, a sandbox flag — accepts only its documented values; anything else makes the declaration throw invalid_credentials rather than fall back to a default host.
Without a broker, a plugin signs locally from its flat CLI config; the selection's localSecret names which flat fields (or fixed values) make up the stored shape.
A target with socket: true is a wss:// endpoint, such as a browser's CDP socket. The broker's optional socketUrl({ target, path }) returns a URL the plugin may open: the local signer returns it signed by the method's query placements, the only placements such a target may have; a host keeping the credential returns a relay it controls. A broker without socketUrl serves no sockets, and plugins refuse those actions as unsupported_operation.
A target may allow the WebDAV methods MKCOL, COPY and MOVE. A COPY or MOVE names its destination, a path beneath the same target checked as path is, and the transport alone sends it as the absolute Destination header; a caller never sets that header.
A target that the provider holds open longer, or answers larger, than the transport defaults declares its own timeoutMs or maxResponseBytes; the host caps both with maxTargetTimeoutMs and maxTargetResponseBytes on CredentialTransport.
Every target of an awsSigV4 method names the service and region it is signed for, and no other method's target may; none is a socket. SigV4 owns Authorization, X-Amz-Date, X-Amz-Security-Token and X-Amz-Content-Sha256, and refuses a presigned URL's X-Amz-* query parameters; the query goes out in the canonical encoding it was signed in. An STS refusal of the key itself (InvalidClientTokenId, SignatureDoesNotMatch, an expired session token) is reconnect_required; any other refusal to assume the role is provider_rejected. Signing locally, an assumeRole connection keeps its session in the flat field awsSession, bound to the key and role it was issued for. A selection's awsProfile (local signing only, ignored by hosts, like localSecret) takes the key from an AWS CLI profile through aws configure export-credentials, held until five minutes before it expires; a CLI with no login is reconnect_required, mended by aws sso login.
runline exec "<code>" # execute JS in the QuickJS runtime
runline exec -f ./script.js # execute a file
runline actions # list all actions
runline actions --connected # only actions for configured connections
runline plugin install <source> # install from git/npm/local
runline plugin list # list installed plugins
runline plugin remove <name> # remove a plugin
runline connection add <n> -p <plugin> -s key=val # add connection
runline connection list # list connections
runline connection remove <name> # remove a connection
runline init # create .runline/ directory.runline/config.json:
{
"connections": [
{ "name": "gh", "plugin": "github", "config": { "token": "ghp_xxx" } },
{ "name": "bf", "plugin": "brandfetch", "config": { "apiKey": "xxx" } }
],
"timeoutMs": 30000,
"memoryLimitBytes": 67108864
}Env vars override config values. Plugins declare env var names in their connection schema (e.g. GITHUB_TOKEN).
Runline is a bun workspace monorepo: packages/runline (library + CLI), packages/runline-plugins (204 built-in plugins, bundled into runline's dist at build time), and packages/pi-runline (pi extension that exposes runline to agents).
bun install
bun --filter runline dev -- exec 'return 1 + 2'
bun --filter runline test
bun run checkThe pi-runline package is a pi extension that plugs runline into coding agents as a single native tool:
execute_runline— run JavaScript in the runline runtime. Discovery happens inside the runtime viaactions.list / find / describe / check, so the agent never needs a separate listing tool.
It ships with /runline-plugins, a fuzzy multi-select picker for choosing which of the 202 plugins the agent should see, plus a guided credential prompt for the ones you enable.
pi install npm:pi-runline
# then in any pi session inside a project with .runline/
/runline-pluginsSee packages/pi-runline/README.md for details.
dripline is query mode — SQL tables over live APIs. runline is code mode — JavaScript actions over the same APIs. Same plugin architecture, same connection config, different interface. Use dripline when you want to SELECT rows; use runline when you want to create, update, delete, or chain multiple API calls together.
MIT