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.inputs
  • ctx.request
  • ctx.env
  • ctx.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.env mirrors the local process environment for convenience.
  • In deployed code nodes, ctx.env is intentionally limited to Brickr metadata such as isDev, version, and gitSha.

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.env
  • require(...)
  • 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.

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.

  • Keep nodeKey stable 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.env as 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 |