stackbone.secrets

Typed reads of encrypted secrets. Operators register API keys, tokens and credentials in the Studio dashboard; your code reads them at runtime through the ambient stackbone client without ever seeing the ciphertext-at-rest.

Mental model

stackbone.secrets is the read side of your agent's credentials. It is agent-local: the operator registers API keys, tokens and credentials in the Studio dashboard, Stackbone stores them encrypted in your agent's own database, and stackbone.secrets reads them straight out of that database and decrypts them locally with the per-agent key the runtime injects as STACKBONE_SECRET_KEY. The ciphertext-at-rest never leaves your agent.

You read secrets wherever you need a credential — from inside an agent tool, or from a durable workflow 'use step'. There is no createClient() and no per-invoke handle to wire up: the ambient stackbone client resolves these credentials lazily from the runtime-injected environment, so you just import it and call.

import { stackbone } from '@stackbone/sdk';

const result = await stackbone.secrets.get('STRIPE_API_KEY');

A secret read is a direct query against the agent's own database followed by a local decrypt — no control-plane round-trip and nothing to negotiate, so it is available from the very first call in any tool or workflow step. The facade does not cache the values, so every call re-reads the database and secret rotations propagate on the next read. Because the read goes through the agent database, your agent must have its Postgres connection configured (the runtime injects STACKBONE_POSTGRES_URL for you). The same surface is available to every agent and workflow in your workspace and to your Stackbone Connect calls — they all read from the same install's database.

Read a single secret

Read one secret from inside an agent tool. get returns a Result envelope — check .error before touching .data, and never log a decrypted value.

import { tool } from '@langchain/core/tools';
import { stackbone, z } from '@stackbone/sdk';

const createCharge = tool(
  async ({ amountCents }: { amountCents: number }) => {
    const result = await stackbone.secrets.get('STRIPE_API_KEY');
    if (result.error) throw new Error(result.error.code);

    // Pass the value straight to the downstream client — don't log it.
    const stripe = createStripeClient(result.data);
    const charge = await stripe.charges.create({ amount: amountCents });
    return JSON.stringify({ chargeId: charge.id });
  },
  {
    name: 'create_charge',
    description: 'Create a Stripe charge.',
    schema: z.object({ amountCents: z.number() }),
  },
);

get rejects empty names with secrets_invalid_request. A name that has not been registered for this agent surfaces secrets_not_found.

Read many at once

stackbone is the same ambient client everywhere — getMany works identically from a tool's execute() and from a workflow step.

const result = await stackbone.secrets.getMany([
  'STRIPE_API_KEY',
  'STRIPE_WEBHOOK_SECRET',
  'POSTMARK_TOKEN',
]);
if (result.error) throw new Error(result.error.code);

const stripeKey = result.data.STRIPE_API_KEY; // string | undefined

Names absent from the agent come back as omissions in the returned map — getMany never fails just because one secret is missing. Callers that need all-or-nothing semantics should diff the keys themselves:

const requested = ['A', 'B', 'C'];
const present = Object.keys(result.data);
const missing = requested.filter((k) => !present.includes(k));
if (missing.length > 0) throw new Error(`Missing secrets: ${missing.join(', ')}`);

A secret read is also a fine thing to do inside a durable workflow. A 'use step' function runs once, persists its result and retries on failure, so wrap the credentialed work — fetching the secret and calling the third-party API — in a step so a transient failure replays cleanly:

import { stackbone } from '@stackbone/sdk';

async function chargeCustomer(amountCents: number) {
  'use step';
  const result = await stackbone.secrets.get('STRIPE_API_KEY');
  if (result.error) throw new Error(result.error.code);

  const stripe = createStripeClient(result.data);
  const charge = await stripe.charges.create({ amount: amountCents });
  return { chargeId: charge.id };
}

Errors

stackbone.secrets reads the agent database and decrypts locally, so its failures are database- and crypto-shaped rather than HTTP-shaped.

Code When
secrets_invalid_request Empty name, or an empty names array.
secrets_not_found get(name) and the agent has no secret with that name.
secrets_decrypt_failed The stored ciphertext could not be decrypted with the per-agent key.
secrets_not_configured STACKBONE_SECRET_KEY is missing, or the per-agent cipher could not be built from it.
secrets_unavailable The read against the agent database failed.
database_not_configured The agent has no Postgres connection configured (STACKBONE_POSTGRES_URL unset / agent database missing).

Where to go next

  • stackbone.config — for non-secret per-agent config (feature flags, toggles, JSON snippets).
  • stackbone.approval — pair with stackbone.secrets to gate a paid API call on a human approval.
  • Agents — where agent tools live, the most common place to read a secret.

The platform for agent developers.Build, ship and observe agentsthat survive prod.

© 2026 · STACKBONEBUILT WITH ❤️ FROM CANADA AND SPAIN