Custom Code SDK
> @brickr/sdk is the package you use inside a Brickr custom code project to define nodes, types, enums, structures, and secrets.
The SDK is published as:
npm install @brickr/sdk
Most projects do not install it manually because brickr init adds it for you.
AI and runtime contract
If you are an AI agent or template author generating Brickr code, follow this contract first and then fill in business logic.
| Rule | What to do | |------|------------| | Target the deployed runtime | Write code for Brickr's Worker-style runtime, not for a Node.js server | | Return plain objects | Keep run(ctx) centered around return { ... } with output keys matching the declared outputs | | Use Brickr APIs for Brickr data | Use ctx.secret(...), t.enumValue(...), t.struct(...), and brickr.config.ts instead of inventing parallel config systems | | Do not assume raw env access | Do not expect direct access to Worker env, service bindings, or internal runtime variables | | Do not use Node-only APIs | Avoid fs, path, require, process.env, sockets, or filesystem access in deployed nodes | | Keep logic portable | Prefer deterministic transforms over runtime tricks or dynamic code generation |
Execution runtime
Deployed Brickr code nodes run inside Brickr's Cloudflare Worker-based execution environment.
That means:
- the mental model is closer to Web / Worker APIs than to Node.js
- direct access to the underlying execution engine is intentionally restricted
- the supported integration path for credentials is
ctx.secret(...) - the supported integration path for shared types is
brickr.config.ts
What is stable
At the Brickr SDK level, these are the stable interfaces you should code against:
ctx.inputsctx.requestctx.envctx.secret(...)ctx.log(...)ctx.fail(...)
What ctx.env is for
ctx.env is not a raw pass-through to the Worker's internal environment.
Use it only for Brickr-provided runtime metadata.
- In local dev,
ctx.envmirrors the local process environment for convenience. - In deployed code nodes,
ctx.envis intentionally limited to Brickr metadata such asisDev,version, andgitSha.
This is by design. Secrets and infrastructure bindings should not leak in from the surrounding execution engine.
What not to assume
Do not generate Brickr code that depends on:
process.envrequire(...)- direct filesystem access
- Node.js core modules
- direct access to Cloudflare
env - direct D1, KV, R2, or service-binding handles
If a node needs credentials, use workspace secrets. If it needs shared data contracts, use Brickr enums and structures.
Worker-style code vs. maximum compatibility
The intended direction is Worker-style JavaScript, not Node.js.
In practice, the most reliable cross-runtime form today is still:
- pure
run(ctx)logic - deterministic data transformation
- explicit return-object output
- Brickr-managed secrets and types
If you keep the node centered around ctx inputs, ctx.secret(...), plain JS transforms, and return { ... }, it stays compatible with Brickr's safer execution path.
About fetch() and other runtime globals
Think in terms of Web / Worker APIs, not Node.js APIs.
That means fetch, Request, Response, URL, and Headers are the right mental model, while fs, path, require, and process.env are not.
But there is one important distinction:
- Target model: Worker-style code
- Most reliable guaranteed subset today:
run(ctx)logic that transforms inputs and returns a plain object
So if an AI agent is generating Brickr code, the safest default is not "write arbitrary server code". The safest default is:
- declare strict pins
- read secrets through
ctx.secret(...) - use Brickr enums and structures
- return a plain object
If you need more runtime-heavy behavior such as network access, validate it in the dev channel early and avoid coupling the node to Node.js assumptions.
Basic node definition
The core entry point is defineCodeFunctionNode(...).
import { defineCodeFunctionNode, t } from "@brickr/sdk";
export default defineCodeFunctionNode({
nodeKey: "acme.slugify-title",
name: "Slugify Title",
description: "Convert a title into a URL slug",
category: "text",
inputs: {
title: t.string().description("Source title").default("Hello World"),
lowercase: t.boolean().default(true)
},
outputs: {
slug: t.string()
},
run(ctx) {
const title = String(ctx.inputs.title ?? "");
const normalized = title.trim().replace(/\s+/g, "-");
return {
slug: ctx.inputs.lowercase ? normalized.toLowerCase() : normalized
};
}
});
Every node module should default-export exactly one defineCodeFunctionNode(...) result.
defineCodeFunctionNode fields
| Field | Required | Description | |-------|----------|-------------| | nodeKey | yes | Stable identifier for the node. Use a namespaced format such as acme.slugify-title. | | name | yes | Builder label shown to users. | | description | no | Short usage description. | | category | no | Node category shown in the Builder. Defaults to code-function. | | inputs | no | Map of input pin definitions. | | outputs | no | Map of output pin definitions. | | run | yes | Function that receives the execution context and returns the output object. |
The runtime expects run(ctx) to return a plain object whose keys match your defined outputs.
Recommended code shape
The best Brickr code node shape is:
1. Declare all inputs and outputs explicitly 2. Reference named enums and structures instead of anonymous loose objects 3. Pull credentials through ctx.secret(...) 4. Return one plain object matching the output contract
Example:
import { defineCodeFunctionNode, t } from "@brickr/sdk";
export default defineCodeFunctionNode({
nodeKey: "acme.create-order-preview",
name: "Create Order Preview",
category: "orders",
inputs: {
order: t.struct("Order"),
status: t.enumValue("OrderStatus"),
includeSecretPreview: t.boolean().default(false)
},
outputs: {
preview: t.object(),
statusLabel: t.string()
},
run(ctx) {
const order = ctx.inputs.order as Record<string, unknown>;
const apiKey = ctx.secret("STRIPE_API_KEY");
return {
preview: {
id: order?.id ?? null,
status: ctx.inputs.status,
hasStripeKey: !!apiKey,
secretPreview: ctx.inputs.includeSecretPreview ? `${apiKey.slice(0, 4)}...` : null
},
statusLabel: String(ctx.inputs.status ?? "")
};
}
});
Pin type builders
The t namespace creates typed Brickr pins.
| Builder | Produces | Notes | |---------|----------|-------| | t.string() | string | String input/output | | t.number() | number | Numeric input/output | | t.boolean() | boolean | Boolean input/output | | t.any() | any | Unrestricted value | | t.object() | object | Generic object | | t.object("Name") | object:Name | Named object type | | t.struct("Name") | object:Name | Explicit structure alias that requires a name | | t.enumValue("Name") | enum-value:Name | Enum-backed value | | t.array(inner) | type:array | Array of another Brickr type builder |
Examples:
inputs: {
email: t.string(),
retries: t.number().default(3),
metadata: t.object(),
order: t.struct("Order"),
status: t.enumValue("OrderStatus"),
tags: t.array(t.string()),
headers: t.array(t.struct("HttpHeader"))
}
Pin modifiers
All standard pin builders return a PinBuilder, so you can chain modifiers.
| Modifier | What it does | |----------|--------------| | .optional() | Marks the pin as optional | | .description(text) | Adds Builder-facing pin help text | | .default(value) | Sets the default pin value | | .portOnly(enabled?) | Marks the pin as connect-only, with no inline value UI | | .pinOnly(enabled?) | Alias of .portOnly(...) | | .allowInlineValue(enabled?) | Re-enables inline value entry on a port-only pin |
Example:
outputs: {
debug: t.any().optional().description("Raw upstream payload")
}
portOnly, pinOnly, and inline values
The SDK currently models "pin only" as the same concept as "port only". Both mean the Builder should treat that pin as connection-driven rather than value-entry-first.
Use:
t.string().portOnly()
or:
t.string().pinOnly()
If you need to explicitly allow inline values again:
t.string().portOnly().allowInlineValue(true)
Sequence pins
Use the seq helper when you want explicit sequence-style pins:
import { defineCodeFunctionNode, seq, t } from "@brickr/sdk";
export default defineCodeFunctionNode({
nodeKey: "acme.with-sequence",
name: "With Sequence",
inputs: {
in: seq.in(),
value: t.string()
},
outputs: {
out: seq.out(),
result: t.string()
},
run(ctx) {
return {
result: String(ctx.inputs.value ?? "")
};
}
});
Sequence pins are port-only by default.
Execution context
run(ctx) receives a CodeFunctionRunContext.
| Property | Description | |----------|-------------| | ctx.inputs | Resolved input values for the node | | ctx.request | Request metadata if the runtime provides it | | ctx.env | Brickr runtime metadata exposed by the runtime | | ctx.secret(name) | Read a required secret by name | | ctx.log(...args) | Write debug logs | | ctx.fail(message, code?) | Throw a structured node failure |
ctx.inputs
ctx.inputs contains your runtime inputs keyed by pin id.
run(ctx) {
const amount = Number(ctx.inputs.amount ?? 0);
return { total: amount * 1.19 };
}
ctx.request
The request object is optional and includes:
{
method?: string;
url?: string;
headers?: Record<string, string>;
}
This is useful when your node behavior depends on HTTP metadata.
ctx.secret(name)
Use ctx.secret("NAME") instead of hard-coding credentials:
run(ctx) {
const apiKey = ctx.secret("STRIPE_API_KEY");
return { preview: apiKey.slice(0, 4) + "..." };
}
In local dev, this resolves from the environment variable of the same name. In a linked workspace, the runtime resolves it from the workspace secret store.
ctx.log(...args)
Use ctx.log(...) for temporary debugging or structured execution output:
run(ctx) {
ctx.log("input", ctx.inputs);
return { ok: true };
}
brickr dev prints these logs in the terminal after each local run.
ctx.fail(message, code?)
Use ctx.fail(...) when you want a clean, explicit failure instead of returning partial output:
run(ctx) {
if (!ctx.inputs.email) {
ctx.fail("email is required", "MISSING_EMAIL");
}
return { ok: true };
}
Named assets
The SDK also lets you define workspace assets in code so your nodes share the same types and secret requirements.
defineEnum(...)
import { defineEnum } from "@brickr/sdk";
export const OrderStatus = defineEnum({
name: "OrderStatus",
description: "Allowed order states",
values: [
"draft",
"paid",
{ key: "shipped", value: "shipped", label: "Shipped" }
]
});
defineStructure(...)
import { defineStructure } from "@brickr/sdk";
export const Order = defineStructure({
name: "Order",
description: "Order payload used by code nodes",
fields: [
{ name: "id", type: "string", required: true },
{ name: "status", type: "enum-value:OrderStatus", required: true },
{ name: "tags", type: "string:array" }
]
});
defineStructure currently uses raw Brickr type strings in fields, not t.* builders.
defineSecret(...)
import { defineSecret } from "@brickr/sdk";
export const StripeApiKey = defineSecret({
name: "STRIPE_API_KEY",
description: "Server-side Stripe secret key",
required: true
});
brickr.config.ts
Expose code-managed assets through the project config:
import { collectAssets, defineEnum, defineSecret, defineStructure } from "@brickr/sdk";
const OrderStatus = defineEnum({
name: "OrderStatus",
values: ["draft", "paid", "shipped"]
});
const Order = defineStructure({
name: "Order",
fields: [
{ name: "id", type: "string", required: true },
{ name: "status", type: "enum-value:OrderStatus", required: true }
]
});
const StripeApiKey = defineSecret({
name: "STRIPE_API_KEY",
description: "Workspace Stripe key"
});
const assets = collectAssets([OrderStatus, Order, StripeApiKey]);
export default {
projectName: "main",
...assets
};
Then sync the file with:
brickr assets sync
Working correctly with enums, structures, and secrets
The cleanest Brickr setup is to keep all three layers aligned:
1. Define shared enums and structures in brickr.config.ts 2. Sync them with brickr assets sync 3. Reference them from node pin types with t.enumValue(...) and t.struct(...) 4. Define secret requirements in brickr.config.ts 5. Read secret values at runtime with ctx.secret(...)
That gives you one consistent source of truth:
- Builder pin types match the code
- workspace assets match the project
- runtime secrets stay outside source code
Node.js vs. Brickr Worker runtime
Use this as a hard rule when writing code:
| Use | Avoid | |-----|-------| | plain JS / TS transforms | filesystem APIs | | ctx.inputs | process.env in deployed code | | ctx.secret(...) | hard-coded credentials | | Brickr enums and structures | anonymous ad-hoc payload contracts everywhere | | Worker-style request metadata | Node-specific modules and globals |
Advanced helpers
buildManifestFromNode(node)
Returns the publishable manifest shape without the run function. This is mainly useful for tooling, testing, or custom build scripts.
collectAssets(assets)
Takes a mixed list of enums, structures, and secrets and returns:
{
enums: [...],
structures: [...],
secrets: [...]
}
This is the easiest way to keep brickr.config.ts compact when you define many assets.
Recommended conventions
- Keep
nodeKeystable even if the display name changes. - Use short categories that make sense in the Builder search UI.
- Return an object with output keys that exactly match your
outputs. - Keep named structures and enums in
brickr.config.ts, not spread across random node files. - Use
ctx.secret()for credentials instead of embedding them in source. - Treat
ctx.envas Brickr metadata, not as a backdoor to the raw execution engine.
What's next?
| Topic | Description | |-------|-------------| | Overview | Big-picture lifecycle for custom code nodes | | CLI | Commands for local testing, publish, sync, and updates | | Workflow | End-to-end authoring flow and troubleshooting | | Type System | Broader Brickr type system background |