Integrate BrixLab into your SaaS or PaaS

BrixLab gives your customers a way to build custom behavior inside boundaries you define. This guide starts with SDK initialization on your backend, then walks through a customer generating, configuring and publishing tools that your AI agent can call.

Your application owns sign-in, tenant membership, permissions and the agent conversation. BrixLab owns implementation authoring, validation, secure setup, versioning and isolated execution. One Brix contract can have many tenant implementations, each exposing several tools.

1. Install and initialize the SDK

Use Node.js 22 or newer. The SDK connects to the single hosted BrixLab service at https://dash.brixlab.dev. Your SaaS or PaaS supplies an access token; no endpoint setting or local BrixLab server is required.

Use a management key for your BrixLab account, provisioned by its operator. Installing the SDK does not create an account or access key. Keep account keys on your backend.

Install the published brixlab SDK (this guide targets SDK 0.8.0):

npm install brixlab

Set one variable in your backend's environment or secret manager. Load it through your framework's server configuration; plain Node does not automatically load a .env file.

BRIXLAB_TOKEN=<your-account-management-key>

Create a server-only module, for example server/brixlab.ts:

import { BrixLab } from 'brixlab';

function requiredEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing server environment variable: ${name}`);
  return value;
}

export const owner = new BrixLab({
  token: requiredEnv('BRIXLAB_TOKEN'),
});
export const toolsBrixKey = 'agent-custom-tools';

Keep this module out of your browser bundle. Your frontend calls authenticated routes on your SaaS backend; the backend calls BrixLab. Arbitrary cross-origin browser SDK calls are not enabled. BRIXLAB_ENDPOINT is exported for links and direct REST clients; it is built into the package, not an environment variable to configure.

Upgrading from 0.2

Remove the url constructor option and any endpoint environment variable. Initialize owner, tenant-authoring and execution clients with their tokens. Use the complete url returned by authoring.setupLink() instead of combining its path with a configured origin. Existing core contracts, tenant IDs, implementation IDs and access keys remain valid on the main service. The earlier pilot and demo APIs are retired.

Service contributors can run the workspace locally using the operations guide. Customer integrations always use the hosted endpoint.

Inspect SDK-created data in the workspace

Sign in to the BrixLab dashboard with a magic link, email and password, Google or GitHub, then open Brix from the sidebar. You see the workspace of your active account. Switch accounts from the account menu; invite teammates and view the account's products and plans in Settings. The dashboard requires sign-in; access keys are for the SDK and REST API. Contracts saved by define() appear in the Brix selector, including contracts with no implementations yet. View saved contract shows the stored capabilities, schemas, and constraints.

The tenant selector discovers tenants from saved implementations and shows total and published counts. Select a tenant to see its drafts and published implementations. Setup shows current draft configuration values and whether credentials are configured, without revealing secret values. History shows published versions and the latest 100 tests and published executions, including SDK calls. Published versions retain their own configuration snapshots.

Use Refresh workspace after creating contracts or implementations through the SDK; select an implementation or click Reload to inspect its latest draft. Signed-in members see all tenants in the account. Through the API, an account management key can discover all tenants, a tenant management key is restricted to its tenant, and an execution key cannot manage implementations. Requests return 403 PRODUCT_INACTIVE when the account's Brix product is not active. No separate registration in the dashboard is required.

2. Define your extension point and provision tenants

Run the following during platform initialization, using the owner client from step 1:

await owner.define({
  key: toolsBrixKey,
  usage: 'ai-tools',
  capabilities: {
    multipleTools: true,
    externalHttp: true,
    secrets: true,
    dynamicConfig: true,
  },
});

Grant only the capabilities your use case needs. A contract defines capabilities and resource limits; it does not choose customers' API domains. Contracts are immutable, so capability changes require a new contract key. define() creates the extension point; use(key) returns its SDK handle.

Implementation destinations and credential approval

The AI determines authentication and business API hosts from the supplied API documentation and declares them in the implementation. Different implementations of the same Brix can use different providers without changing the shared contract.

Setting Purpose
Contract capabilities.externalHttp Allows implementations to request external HTTP.
Artifact network.allowedHosts Exact public DNS names this implementation proposes to contact.
Secret field hosts and usage Where a credential may be used, and whether it is for direct requests, token exchange, or signing.
Saved setup approval User approval of those destinations and authentication recipes, stored independently from generated code.

Use lowercase DNS names without URLs, ports, wildcards or implicit subdomains. Requests require HTTPS on port 443 and never follow redirects. The gateway also blocks access to private networks. Include both authentication and business API hosts, with the correct sandbox or production domains.

The user reviews these permissions in secure setup, enters credentials, runs tests, and publishes. An artifact edit cannot approve new hosts or authentication recipes. Changes to network hosts, credential hosts/usage, or any token exchange recipe require another review; ordinary code edits still require fresh tests. Credentials remain encrypted when permissions change, but cannot be used until the new permissions are approved. Removing or retyping a field discards its draft value. Published versions retain their own credential and approval snapshots.

Legacy contract constraints.allowedHosts is accepted only for reading/repeating old definitions and is not enforced by the v2 runtime. New definitions should omit it.

When onboarding a tenant, issue separate authoring and execution keys:

// tenantId comes from your authenticated tenant provisioning workflow.
const manageKey = await owner.issueKey({ tenantId, role: 'manage' });
const executeKey = await owner.issueKey({ tenantId, role: 'execute' });
// Persist manageKey.token and executeKey.token in your server-side secret store.
// Save them once per onboarding workflow, not on every incoming request.

On subsequent requests, load those keys from your application's secret store:

const tenantTools = new BrixLab({
  token: tenantManageToken,
}).use(toolsBrixKey);
const runtime = new BrixLab({
  token: tenantExecuteToken,
}).use(toolsBrixKey);

Here and below, tenantId, tenantManageToken and tenantExecuteToken are values resolved by your backend, not SDK globals. Derive the tenant from the signed-in user's authorized workspace; do not trust a tenant ID supplied in a browser request. Store session and implementation IDs with that tenant. Tenant keys cannot define contracts, configure providers or issue keys. They are tenant-scoped, not scoped to a single Brix or implementation; your application decides which users and agents may manage or execute each implementation.

3. Let users generate custom AI tools

For an AI SaaS, add a Custom tools page to each workspace. For a PaaS, place it in the authorized tenant/project workspace and map that workspace to a BrixLab tenant. Provide a prompt editor, documentation input, generation feedback, setup, test inputs and a Publish action.

A customer might ask: “Create a quote lookup and a company search tool using this API. Ask for my API key securely.” They can refine the draft over several turns. One implementation can expose both tools; your platform does not need a custom connector for each one.

Hosted generation

Hosted sessions use BrixLab's platform provider by default. Your SaaS needs only its BrixLab token: omit model when creating a hosted session. The platform operator supplies the model credential; no provider key or dashboard setup is required in your SaaS.

An account owner can optionally override the platform default with their own provider on the backend or owner dashboard:

await owner.configureProvider({
  provider: 'openai',
  name: requiredEnv('BRIXLAB_MODEL'), // coding model
  planner: 'gpt-5.4', // optional, same provider
  reviewer: 'gpt-5.4', // optional, defaults to planner
  apiKey: requiredEnv('MODEL_API_KEY'),
});

BRIXLAB_MODEL and MODEL_API_KEY are needed only for this optional account override. OpenAI models must support Chat Completions and JSON mode; Anthropic models must support Messages (use provider: 'anthropic'). The adapter requests up to 12,000 completion tokens for coding and 16,000 for planning/review. OpenAI GPT-5/o-series planning uses medium reasoning effort; section selection and focused independent review use low reasoning effort. Section selection has a 30-second timeout, coding 60 seconds, and reasoning-based planning/review 90 seconds; one code repair is allowed. Large linked OpenAPI references add a section-selection call, so a hosted turn can make up to six model calls. For account overrides, an omitted planner uses the configured coding model; an omitted reviewer uses the planner. Platform-funded requests use the operator's selected model. An account override takes precedence and is never silently retried with platform credentials if it fails. New sessions snapshot all three model selections; older sessions acquire stage selections on their next hosted turn. Start a new session after changing providers or platform models; models are never silently substituted.

To inspect configuration without returning credentials, call await owner.request('authoring/provider-status', {}). The response contains configured, source (platform, account, or null), and model, plus models (planner, coder, reviewer). This reports saved configuration; a successful generation verifies actual provider access. Run examples/hosted-ai.mjs to generate, test, publish and execute two tools through tenant SDK clients, with generation and execution timings.

Inside an authenticated Create tools route, use the tenant authoring client:

const session = await tenantTools.authoring.create({
  tenantId,
  mode: 'hosted',
  documentation: [apiDocumentation],
});
// Persist session.id with your tenant before requesting generation.
const state = await session.prompt(userPrompt);
// Return session.id and state to your workspace UI.
// Show state.validation, state.draft and state.messages.

apiDocumentation accepts a public documentation URL, a bundled OpenAPI JSON/YAML URL, or pasted documentation text. Hosted prompts retrieve links before planning. userPrompt is the user's requested behavior. Credentials belong in secure setup, never in the prompt or documentation. The model receives the contract's constraints and the current authoring context.

For the next chat turn, load the saved session instead of starting over:

const resumed = await tenantTools.authoring.resume({ tenantId, sessionId });
const updated = await resumed.prompt(nextUserMessage);

Sessions retain the conversation, documentation, draft reference, validation and current resource test results. Invalid output preserves the previous draft and adds validation feedback. Provider failures preserve draft/session revision; concurrent edits return a conflict. Refresh or resume before retrying. Successful generation does not publish automatically.

When generated JSON has missing fields, wrong types, or extra properties, state.validation.code is INVALID_ARTIFACT. Its message is a short user-facing reply; details.message retains the full explanation, and issues contains up to 20 {path, code, message} entries (for example, artifact.resources[0].inputSchema). Show message in the conversation and put details/issues in an expandable panel. Structured diagnostics are also supplied to the models on the next prompt. Rejected artifacts are not saved. Unexpected storage or server failures return an HTTP 500 service error instead of artifact-validation feedback.

Documentation planning and review

Hosted generation creates a structured state.plan, generates code, then independently reviews the plan and implementation against the original documentation. Platform defaults are gpt-5.4 for planning/review and gpt-4.1 for coding. The plan belongs to the implementation, never the shared Brix contract.

The plan records authentication, all required credentials, token requests, destinations, operations, test expectations and source quotes. Evidence points into documentation or user messages using zero-based indices. The service checks that the quoted text exists and copies setupSchema, network, tokenExchanges and matching operation descriptions from the plan into the artifact. The coding model cannot omit a planned credential. Artifact validation checks agreement; a separate model reviews whether the plan itself matches the sources. Code-only failures get one repair and a fresh review. Model review is fallible and does not replace runtime tests.

OAuth 2.0 and a Bearer header alone do not specify how to obtain a token. The planner identifies the documented grant, credential fields, encoding, endpoint, scopes and token path. If docs require a key/client ID and secret, both appear in secure setup and in the exchange. A documented direct API-key API still needs only its documented inputs. Unsupported flows, including interactive authorization and combined Basic client authentication, are reported explicitly.

Check state.validation.valid before using a draft. Failures include DOCUMENTATION_FETCH_FAILED, DOCUMENTATION_EMPTY, DOCUMENTATION_RENDER_FAILED, DOCUMENTATION_URL_BLOCKED, DOCUMENTATION_FORMAT, DOCUMENTATION_LIMIT, CLARIFICATION_REQUIRED, AUTH_UNSUPPORTED, PLAN_EVIDENCE_INVALID, PLAN_MISMATCH and REVIEW_FAILED, with stage and optional structured issues. Missing authentication details produce questions rather than a guessed API-key implementation. Previous accepted drafts remain intact. state.plan is the latest proposal and may not have passed review; an accepted artifact retains its own implementationPlan.

Hosted URL reading accepts up to three public HTTPS documentation links per session. Use standalone URLs, Markdown links or explicit documentation links in prose. API endpoint URLs in pasted request examples are not automatically called. The reader prefers linked OpenAPI JSON/YAML (including Redoc/Swagger app configuration), then readable HTML/text and nearby authentication/sandbox/operation pages. A JavaScript-only page uses an isolated browser fallback. This is bounded documentation retrieval, not an unrestricted site crawler. PDFs, signed/private links, login-protected pages and OpenAPI files with external references require pasted text or a bundled public specification.

state.sources records each requested URL, actual source URL, retrieval time, format and read/failure status. For successful reads, documentationIndex points to an immutable snapshot in state.documentation; plan evidence uses those same indices. For large OpenAPI files, the planner selects relevant operations; authentication paths, global security, server overrides and all referenced schemas are retained automatically. Planner, coder and reviewer receive the same selected content; citations are checked against the full snapshots. Reopening a session reuses its snapshots. Start a new session to refresh successful sources. Supplying replacement documentation on a follow-up prompt supersedes previously failed links; retrying without replacement reattempts them.

A retrieval failure returns stage: "documentation" and its specific code before a model is called. It does not claim the API documentation lacks authentication details. CLARIFICATION_REQUIRED is reserved for information still missing after successful retrieval. Replies use the request language (built-in retrieval messages support Hebrew and English), stay short, and ask one essential question. Full causes, questions and source evidence remain available in details. Never include credentials in documentation or prompts.

Use your own AI backend instead

For a SaaS/PaaS with an existing generation service, use an external session. Your own service generates an artifact; BrixLab applies the same validator and lifecycle:

const context = await tenantTools.authoring.context();
const externalSession = await tenantTools.authoring.create({
  tenantId, mode: 'external', documentation: [apiDocumentation],
});
await externalSession.prompt(userPrompt);
const artifact = await myModel.generate({
  system: context.instructions,
  contract: context.contract,
  schema: context.implementationSchema,
  messages: externalSession.state.messages,
  documentation: externalSession.state.documentation,
  currentImplementation: externalSession.state.draft?.artifact,
});
await externalSession.submit(artifact);

External backends also receive planningInstructions and planSchema from authoring.context(). They own URL retrieval, model selection, extraction and independent review; automatic URL reading runs only for hosted prompts. Include implementationPlan in the artifact for structural agreement checks; session submissions also check source quotes. Direct/manual submissions without a plan remain supported and do not claim documentation review.

myModel.generate is a placeholder for your application's model adapter, not a BrixLab method. Return a parsed artifact object matching the supplied schema. Save and resume the external session for follow-up turns. A session is optional for direct submissions through tenantTools.authoring.submit({tenantId, artifact}).

4. Collect setup, test every tool and publish

The dashboard generates a form from each resource's inputSchema, with descriptions, required fields, enum choices, nested objects and arrays, and an advanced JSON editor. It prefills validated suggestions when available, preserves edits while switching resources or reloading the current draft, and shows field errors before execution. Inputs are kept in memory for the selected implementation; reconnecting or changing implementations clears them. Complete setup, run each resource, inspect its result, then publish when every resource has passed for the current revision.

AI authoring supplies optional per-resource guidance alongside the schemas:

{
  "testing": {
    "exampleInput": { "recordId": "YOUR_RECORD_ID", "limit": 3 },
    "userInputs": [
      { "path": "/recordId", "instructions": "Copy a record ID from your test account." }
    ],
    "effects": "Reads the selected record from the connected service."
  }
}

exampleInput must be a complete, schema-valid JSON value. Omit it when meaningful valid values are unavailable. userInputs identifies fields the user must fill with their own values; paths are RFC 6901 JSON pointers (/recordId, /items/0/id, or an empty string for the entire input). These paths must resolve to declared fields and must be distinct. The form clears these values even when an example contains placeholders. effects describes external changes a test may make. Guidance cannot change a fixed Brix contract and must never contain credentials. Suggestions are never automatically executed and never satisfy publication gates.

For your own UI, use createTestInput(resource) to obtain editable starting values and validateTestInput(resource, input) for {valid, issues} feedback. Both are exported from brixlab. Starting values may be incomplete or undefined; have users supply missing values. Validation checks the schema and nonempty values at userInputs paths, with issues shaped as {path, code, message}. The service repeats these checks before running a test and returns optional error.issues on failed executions. API request errors expose the same optional issues through BrixLabError.issues. These helpers also work with older artifacts that have no testing field.

The following uses the hosted session; for external generation use externalSession. Stop and display validation feedback if generation did not produce a valid draft:

if (!session.state.validation?.valid || !session.state.implementationId) {
  throw new Error('Resolve generation feedback before setup.');
}
const target = { tenantId, implementationId: session.state.implementationId };
const link = await tenantTools.authoring.setupLink(target);
const setupUrl = link.url;
// Return setupUrl only to the authorized user and open it as a dedicated page.

The setup page renders the generated fields and collects secrets directly into BrixLab's encrypted vault. Your SaaS receives setup status, not plaintext credentials. The link expires after ten minutes, is tied to the draft revision, and allows one save. Treat it as a temporary credential and do not log it. After the user saves, offer a Check setup action that reloads the draft; there is no automatic setup callback in the SDK.

The following runs in a later, authenticated Test and publish handler. Resolve target from the implementation associated with the user's workspace. Have the user review each generated tool and supply a JSON test input matching its schema:

const draft = await tenantTools.authoring.draft(target);
if (!draft.setup.ready) throw new Error('Complete the required setup first.');

// Collected from your test form, indexed by the actual generated resource keys.
const inputsByResource: Record<string, unknown> = userTestInputs;
for (const resource of draft.resources) {
  if (!Object.hasOwn(inputsByResource, resource.key)) {
    throw new Error(`Provide a test input for ${resource.name}`);
  }
  const result = await tenantTools.authoring.test({
    ...target,
    expectedRevision: draft.revision,
    resource: resource.key,
    input: inputsByResource[resource.key],
  });
  if (!result.ok) throw new Error(result.error.message);
}
// Publish only when the user has explicitly chosen to publish this draft.
const published = await tenantTools.authoring.publish({
  ...target, expectedRevision: draft.revision,
});

In a UI with separate Test and Publish buttons, retain the tested revision and send it when publishing. Do not silently replace it with a newer revision. Code or configuration edits invalidate tests. Every current resource must pass before publication; BrixLab rejects stale revisions and untested drafts. Tests perform real execution, including HTTP writes: use test accounts for write tools. Optional expected output assertions are available; schema-valid output alone does not prove the user's business logic is correct.

Test the connection independently

After secure setup, offer Test connection when draft.artifact.implementationPlan?.connectionTest is present:

const connection = await tenantTools.authoring.testConnection({
  tenantId, implementationId, expectedRevision: draft.revision,
});
if (!connection.ok) {
  // Display redacted details to the authorized user.
  console.error(connection.error.message);
}

This runs a fixed token exchange or a documented read-only GET through the credential gateway. It never executes generated tool code or creates business records. Exchange-only success proves a token was returned, not that every business operation is authorized. Success returns no token or API response body; exchange error bodies remain withheld. Errors retain their source, status and redacted diagnostics. Logs use mode: 'connection'. Changing the connection recipe requires permission review again.

Connection checks never count as resource tests or unlock publication. Test each tool afterward. If no safe check is documented, connectionTest is null and this call returns CONNECTION_TEST_UNAVAILABLE. REST equivalent: POST /api/test-connection with the same target/revision fields.

5. Connect published tools to your AI agent

At the start of an agent turn, list published implementations for the authenticated tenant. Filter them using your application's agent permissions, then map their resources into the tool format required by your model/framework. Keep the implementation ID, resource key and published version on your server so two implementations with the same resource key cannot collide.

// Your app computes this set from the agent's saved, authorized tool selection.
const enabled = new Set<string>(enabledImplementationIds);
const implementations = await runtime.list({ tenantId });
const routes = new Map<string, {
  implementationId: string;
  resource: string;
  version: number;
}>();
const toolDefinitions = [];
for (const implementation of implementations) {
  if (!enabled.has(implementation.id) || implementation.version === null) continue;
  for (const resource of implementation.resources) {
    const name = `brix_${routes.size}`;
    routes.set(name, {
      implementationId: implementation.id,
      resource: resource.key,
      version: implementation.version,
    });
    toolDefinitions.push({
      name,
      description: `${resource.name}: ${resource.description}`,
      inputSchema: resource.inputSchema,
    });
  }
}

async function executeToolCall(name: string, input: unknown) {
  const route = routes.get(name);
  if (!route) throw new Error('Tool is not enabled for this agent turn.');
  const result = await runtime.run({ tenantId, ...route, input });
  return result.ok
    ? { ok: true, output: result.output }
    : { ok: false, error: result.error };
}

toolDefinitions is a provider-neutral intermediate shape, not an SDK model request. In your model adapter, map name, description and inputSchema to that provider's function/tool declaration fields. Parse tool arguments as JSON, call executeToolCall, and return the structured result with the original model tool-call ID before requesting the next model response. Keep the same routes map for that turn; these aliases are not persistent tool IDs.

For example, the control flow for your existing agent adapter is:

// agent is YOUR model/framework adapter; these are not BrixLab SDK methods.
let finished = false;
for (let step = 0; step < 8; step++) {
  const turn = await agent.next({ messages, tools: toolDefinitions });
  messages.push(turn.message);
  if (turn.toolCalls.length === 0) {
    finished = true;
    break;
  }
  for (const call of turn.toolCalls) {
    const result = await executeToolCall(call.name, call.parsedInput);
    messages.push(agent.toolResultMessage(call.id, result));
  }
}
if (!finished) throw new Error('Agent tool-step limit reached.');

The model chooses from enabled tool metadata; your backend dispatches the call and BrixLab validates the input and output. Tool output is data, not authority to change your agent's instructions or permissions. Keep your application's confirmation rules for actions with side effects. The version argument detects a changed published definition; on a stale version, reload metadata and let the agent reconsider instead of silently retrying a write.

6. Wire the customer-facing routes

The route names below are examples owned by your SaaS; they are not BrixLab endpoints. Every route authenticates the user, derives their tenant and checks permissions for the saved session or implementation.

SaaS route or UI action Backend SDK call UI result
Create tools authoring.create() then session.prompt() Save session ID; show draft and validation
Continue conversation authoring.resume() then session.prompt() Updated conversation and draft
Configure authoring.setupLink() Open the temporary secure setup page
Check setup authoring.draft() Required fields and configured/ready status
Test a tool authoring.test() with the shown revision Output or structured error
Publish authoring.publish() with the reviewed revision Published implementation/version
Select tools for an agent list() Save permitted implementation IDs in your app
Agent tool call run() through the server-side routing map Return output to the model
Diagnose or roll back authoring.logs(), versions(), rollback() Execution history and restored version

In this table, authoring.* means the authoring API on a Brix handle, such as tenantTools.authoring.create(). BrixLab supplies the SDK and a hosted workspace; it does not inject a tool-builder widget into your application. You build these controls using your own UI and authentication. Use this same pattern for an AI SaaS workspace or a PaaS project.

During integration, verify that two tenants cannot see each other's implementations, an unconfigured draft cannot be published, and your agent lists only enabled published tools. The end-to-end success path is: generate two tools, configure credentials, test both, publish, then let the agent select and execute one.

Artifact format

{
  "format": "brixlab/v2",
  "brixKey": "agent-custom-tools",
  "metadata": { "name": "Market tools", "description": "Stock quotes" },
  "capabilities": { "externalHttp": true, "secrets": true },
  "network": { "allowedHosts": ["api.your-provider.com"] },
  "setupSchema": [
    { "key": "apiKey", "label": "API key", "type": "secret", "required": true, "hosts": ["api.your-provider.com"], "usage": "request" }
  ],
  "resources": [{
    "key": "quote", "type": "tool", "name": "Quote", "description": "Get the current stock price",
    "inputSchema": { "type": "object", "properties": { "symbol": { "type": "string" } }, "required": ["symbol"], "additionalProperties": false },
    "outputSchema": { "type": "object", "properties": { "price": { "type": "number" } }, "required": ["price"], "additionalProperties": false }
  }],
  "code": "export default async (input, ctx) => ctx.http.request({url: 'https://api.your-provider.com/quote?symbol=' + encodeURIComponent(input.symbol), headers: {Authorization: 'Bearer ' + ctx.credentials.apiKey}});"
}

code is a single JavaScript module with one default function (input, ctx). ctx.resource identifies the selected resource, so one implementation can dispatch multiple tools. Rules and actions expose one resource each; run() can omit resource for single-resource implementations. No npm imports or host application bindings are available.

Capabilities are opt-in on both contract and artifact. Missing flags mean false. Defaults are 10 resources, 50 ms CPU, 5 seconds wall time and 10 HTTP calls; contracts can constrain those within the service's bounded ranges. HTTP methods include GET, HEAD, POST, PUT, PATCH and DELETE. Requests require HTTPS and an exact allowed host. Redirects are rejected, and non-2xx responses throw. Bodies default to JSON; bodyEncoding: 'form' accepts an object of strings and sets the form Content-Type; bodyEncoding: 'text' accepts a string and an explicit Content-Type. Requests and JSON responses are bounded; HTTP 204 returns null.

The supported JSON Schema keywords are type, properties, required, additionalProperties (boolean), items, enum, const, description, title, minimum, maximum, minLength, maxLength, minItems and maxItems. Every schema node requires type; arrays require items. Unknown keywords and references are rejected. Fixed Brix input/output schemas must match each resource exactly, and values are checked again at runtime.

Dynamic setup and secrets

Fields declare key, label, type, required and optional description. Types are string, number, boolean and secret. Every secret requires hosts, a nonempty subset of the implementation's network hosts, and should declare usage: request (default), exchange, or signing. Ordinary fields have no credential permissions. Retrieve schemas, the permission plan, permissionsApproved, migrationRequired, and ready with authoring.draft(); secret values are never returned.

const implementationId = session.state.implementationId!;
const target = { tenantId, implementationId };
const link = await tenantTools.authoring.setupLink(target);
// Send the authorized user to link.url.
// BrixLab's form writes directly to the vault; your SaaS receives no credential.

Links expire after 10 minutes, are tied to the current draft revision, and allow one successful save. The token is in the URL fragment, is removed from the address bar by the setup page, and is sent in an Authorization header. A stale link must be reissued. Same-origin setup uses a dedicated page rather than an iframe.

For trusted backend integrations, authoring.setup({...target, expectedRevision, config, secrets, approvePermissions: true}) is available. Set approvePermissions only after displaying draft.setup.permissions and obtaining the user's approval. Omitting it saves values without expanding permissions. A stale revision is rejected. Generated authoring output cannot set approval.

Generated code receives ctx.credentials.<fieldKey>, an opaque reference valid only for that execution. It never receives raw vault values. Insert a direct API key using:

const result = await ctx.http.request({
  url: 'https://api.your-provider.com/quote',
  auth: { credential: ctx.credentials.apiKey, header: 'Authorization', prefix: 'Bearer ' },
});

auth accepts exactly one of header or query. Arbitrary strings and field names are not valid references. To insert several credentials or use JSON/form bodies, use credentials: [{credential: ctx.credentials.apiKey, in: 'body', name: '/api_key'}]. Header/query names are literal names; body names are JSON pointers through objects. Optional prefix defaults to empty. All bindings enforce credential host and usage restrictions before sending.

Brokered token exchange

For APIs that exchange login credentials for a token, the AI declares a fixed recipe in the artifact. For example:

{
  "network": { "allowedHosts": ["identity.example.com", "api.business.example.com"] },
  "setupSchema": [
    { "key": "clientId", "label": "Client ID", "type": "secret", "required": true, "hosts": ["identity.example.com"], "usage": "exchange" },
    { "key": "clientSecret", "label": "Client secret", "type": "secret", "required": true, "hosts": ["identity.example.com"], "usage": "exchange" }
  ],
  "tokenExchanges": [{
    "key": "login",
    "url": "https://identity.example.com/token",
    "body": { "grant_type": "client_credentials" },
    "bodyEncoding": "json",
    "credentials": [
      { "credential": "clientId", "in": "body", "name": "/client_id" },
      { "credential": "clientSecret", "in": "body", "name": "/client_secret" }
    ],
    "tokenPath": "/accessToken",
    "tokenHosts": ["api.business.example.com"]
  }]
}

This fragment belongs in a complete v2 artifact with externalHttp and secrets granted. Recipes use POST and may include fixed headers. Use bodyEncoding: 'form' for OAuth servers requiring URL-encoded bodies. The API documentation determines the endpoint, field names and token path.

export default async function (input, ctx) {
  const token = await ctx.http.exchange('login');
  const created = await ctx.http.request({
    url: 'https://api.business.example.com/records', method: 'POST',
    headers: { Authorization: 'Bearer ' + token },
    body: { title: input.title },
  });
  if (created.error || typeof created.id !== 'string') throw new Error('Creation failed');
  return { id: created.id };
}

Secrets and tokens reach code only as placeholder strings. Use a placeholder wherever the API expects the secret: a header value, the URL or the request body. The gateway substitutes the real value when sending, and only after checking that credential's approved hosts and usage; a placeholder in a host name or body field name is rejected. The older auth and credentials request options remain supported.

The gateway retains the token and returns only a reference scoped to tokenHosts. Code cannot override the reviewed exchange URL, body, bindings or destinations, and cannot use exchange-only credentials in generic HTTP requests. Tokens are not cached across executions. Exchanges count against the contract's HTTP budget and execution deadline. Authentication response bodies are withheld even on errors; diagnostics retain status and explain missing token paths.

Request signing and response redaction

For a secret with usage: 'signing', HMAC runs in the gateway: hmac: {credential: ctx.credentials.signingKey, message: canonicalRequest, header: 'X-Signature', hash: 'SHA-256', encoding: 'hex'} (use query instead of header for a query parameter, and prefix for a scheme such as 'HMAC '). SHA-512 and base64 are also supported. The message must be constructed from public data and cannot contain placeholders; neither the key nor signature is returned to code. Other signing schemes are currently unsupported; do not restore raw-secret access as a workaround.

Regular HTTP responses are sanitized before reaching generated code, including known credentials, common credential fields and optional sensitiveResponseFields JSON pointers. Those pointers must resolve to nonempty strings. Use the exchange broker when a response credential is needed for another request. Returned results and diagnostic logs are sanitized again. Approving a host means trusting that remote service with the credentials sent there; BrixLab cannot control a service after it receives an authorized request.

Migrating existing implementations

The v2 credential boundary is a breaking change. Existing v1 implementations that use HTTP or secrets return MIGRATION_REQUIRED; v1 computation/config-only implementations continue to run. Regenerate the draft with format: 'brixlab/v2', move domains into network.allowedHosts, replace secret host with hosts/usage, replace raw ctx.secrets with credential bindings and token recipes, review secure setup, test every resource, and publish. Same-key/same-type saved credentials are retained for review. Old published versions are immutable and cannot gain v2 permissions through a draft edit or rollback; publish a tested v2 version before resuming those tools.

Do not put credentials in prompts, docs, code or ordinary configuration. Known stored secrets and common credential patterns are rejected from authoring submissions. Vault values and config values are omitted from model context. This does not claim to recognize every previously unseen credential a user could paste into arbitrary text.

Test, publish, list and run

let draft = await tenantTools.authoring.draft(target);
const tested = await tenantTools.authoring.test({
  ...target, expectedRevision: draft.revision,
  resource: 'quote', input: { symbol: 'AAPL' }, expected: { price: 189.5 },
});
if (!tested.ok) throw new Error(tested.error.message);
// Omit expected for manual tests of live/dynamic outputs.
// Test every resource successfully at the current revision.
const published = await tenantTools.authoring.publish({ ...target, expectedRevision: draft.revision });
const implementations = await runtime.list({ tenantId });
const tools = implementations.flatMap(implementation => implementation.resources);
// Your agent chooses from these metadata-only definitions.
const result = await runtime.run({
  ...target, resource: 'quote', input: { symbol: 'AAPL' }, version: published.version,
});

Tests call the same runtime and can perform real HTTP side effects. Use test credentials/accounts for writes. The automated suite uses controlled HTTP fixtures. expected is optional; otherwise passing means execution succeeded with schema-valid output. Test coverage is per resource, not a proof of business correctness.

Every code or setup edit invalidates test eligibility by incrementing the draft revision. Publish is atomic and requires all current resources to have passed their latest test. Publication increments the revision and snapshots code, config and encrypted credentials. list() and get() return only current published metadata; list({tenantId,drafts:true}) and authoring.draft() require management access.

run({version}) optionally detects a stale tool definition; it does not execute an arbitrary historical version. Use authoring.versions() and authoring.rollback({...target, version, expectedRevision}) to restore the current published pointer. Rollback preserves the draft and all historical versions.

REST mapping

The service origin is https://dash.brixlab.dev. All JSON APIs use POST, Content-Type: application/json, and Authorization: Bearer …. SDK fields map directly to JSON; extension methods add brixKey.

Concern Endpoint suffix after /api/
Contracts define, contracts
Workspace discovery workspace with {} (management access; account context, contracts, and visible tenant counts)
Account and products account with {} (account, enabled products and plans)
Tenant keys keys
Provider authoring/provider
Hosted provider status authoring/provider-status
Sessions authoring/create, authoring/get, authoring/prompt, authoring/submit
External context authoring/context
Drafts submit, validate, get with draft:true
Setup setup, setup-link
Lifecycle test, publish, versions, rollback
Runtime list, get, run
Diagnostics logs
Direct setup token secure-setup/read, secure-setup/save

Runtime success is {ok:true,output,...executionMetadata}; a failed admitted execution is {ok:false,error,...executionMetadata}. Invalid requests/authentication/conflicts use HTTP 4xx and {error:{code,message}}. Provider/service failures use 5xx. The SDK throws BrixLabError for HTTP errors and returns execution failures for the caller to handle explicitly.

Execution error details

Failed tests and published executions expose the original, redacted error through result.error. Display these details to the authorized end user so they can diagnose API requests. The SDK exports ExecutionError, ErrorDetails, and HttpFailure types. Existing code, message, and optional issues remain available; the additional fields are optional for compatibility with earlier results.

  • source: api for a failed API response, implementation for a generated-code exception or invalid output, or brixlab for a request restriction, network failure, or runtime limit.
  • details.name, details.stack, and details.causes: generated-code exception details, including up to three explicit causes.
  • details.http: up to five recent failed requests, each with its own source, code, message, request method/URL, and response status, status text, and JSON or text body when available.
const result = await extension.authoring.test({
  ...target, expectedRevision: draft.revision, resource: resource.key, input,
});
if (!result.ok) {
  showError(result.error.message);
  showErrorDetails(result.error); // Render as text, never as HTML.
  // Example: result.error.details?.http?.[0].response?.status === 422
}

An unhandled HTTP error uses codes such as HTTP_ERROR (non-2xx), HTTP_INVALID_RESPONSE (invalid JSON or missing declared credential), HTTP_REQUEST_DENIED, HTTP_REQUEST_INVALID, HTTP_NETWORK_ERROR, HTTP_CALL_LIMIT, or TIMEOUT. Generated code receives an Error with code and details from ctx.http.request(). To add context while preserving the cause, use throw new Error('Could not create quote', { cause: error }).

HTTP_REQUEST_DENIED identifies an implementation network destination that was blocked before sending. CREDENTIAL_HOST_DENIED names the credential field, approved hosts and attempted destination. CREDENTIAL_USAGE_DENIED explains when a login/signing credential is used in an ordinary request. CREDENTIAL_UNAVAILABLE indicates a missing, forged or expired reference. PERMISSION_REVIEW_REQUIRED means secure setup must approve the current plan. These BrixLab denials have no upstream response. API failures preserve HTTP status and sanitized response details; authentication response bodies are intentionally withheld.

If generated code replaces the exception without preserving its cause, the top-level error is classified as an implementation error and the failed HTTP calls remain available separately. A previously handled HTTP failure is not necessarily the cause of a later implementation error. Recovered requests can still produce a successful execution.

Known vault values, tracked authentication credentials, and common credential fields are redacted from messages, stacks, response bodies, and saved execution errors. Request headers and request bodies are not included; request URL query values are masked. Response bodies are bounded to 64 KiB; larger or unreadable bodies are omitted with response.bodyOmitted set to too_large or unreadable. Oversized exception fields are replaced with a size-limit message. Error bodies may still contain business data, so only show them within the implementation's authorized tenant scope. Redaction does not cover arbitrary transformations of secrets.

The dashboard shows the error object after a failed test or run. Execution history has an expandable Error details view containing the same saved diagnostics. Earlier executions retain their original errors; their discarded API responses cannot be recovered.