randal
reference / config reference

Config reference

YAML configuration shape and supported fields.

⚙️ Configuration Reference

Randal is configured via a YAML file. Accepted filenames (checked in order):

  1. randal.config.yaml
  2. randal.config.yml
  3. randal.yaml

Or specify explicitly: randal --config path/to/config.yaml <command>

All string values support ${ENV_VAR} substitution from the process environment. The parsed config is deeply frozen (immutable at runtime).

Unknown fields warn-and-ignore at load. Config surface the running version doesn't recognize (e.g. removed or renamed by an app update, like the retired runner.compaction) is dropped with a config: ignoring … warning in the log instead of failing startup — so an older config file never crash-loops the gateway. This covers unknown fields, entries with a retired type (e.g. a removed channel or embedder type — the entry is dropped), and unknown enum values on recognized fields (the field is dropped so its default applies). Genuine type errors on recognized fields still fail hard. Interactive validation (randal init --from) stays strict and reports all of these as errors, so use it to catch typos.


📋 Top-Level

FieldTypeDefaultRequiredDescription
namestringYesInstance name. Used for memory index naming and identification.
versionstring"0.1"NoConfig schema version.
possestringNoTeam/group identifier for multi-agent coordination.
managed.bootstrapobjectNoOptional published-artifact bootstrap metadata. Used only for runtime startup, never as Config Studio state.
capabilitiesstring[][]NoCore/runtime capability hints. Optional agent tools such as Image, Video, and Notion are now configured through connectors.

Published Artifact Bootstrap

managed.bootstrap is optional and does not change normal local startup defaults.

managed:
  bootstrap:
    controlPlaneUrl: https://control-plane.example
    workspaceId: workspace-123
    versionId: version-7
    artifactUrl: https://control-plane.example/api/workspaces/workspace-123/setup-versions/version-7/config-export

Rules:

  • only published artifacts may be referenced here
  • this field is runtime bootstrap metadata, not Studio draft state
  • the compiled runtime behavior must remain unchanged by default when managed.bootstrap is absent

🧩 Capabilities

The capabilities field declares core/runtime capability hints. Memory and scheduler remain core primitives. Optional agent tool packs such as Image, Video, and Notion are configured through connectors instead of hard-coded capability/env detection.

CapabilityMCP Server(s)Description
memorymemoryPersistent memory/chat/DAG access backed by Postgres. Also enabled automatically when memory.store is configured.
schedulerschedulerHeartbeat and cron scheduling. Also enabled automatically when heartbeat.enabled or cron.jobs is non-empty.

Example:

# Explicitly declare all capabilities this agent uses
capabilities:
  - memory
  - scheduler

If capabilities is omitted or empty, core MCP servers are still wired based on other config sections such as memory.store, heartbeat.enabled, and cron.jobs.

Use randal connector enable <id> or the Console Connectors panel for optional guided connectors. In Docker/Railway overlay mode, Console-managed connector state is persisted separately at $RANDAL_STATE_DIR/connectors/overlay.yaml (or RANDAL_CONNECTOR_OVERLAY_PATH) while config-declared connectors remain authoritative over overlay entries with the same id.


🔄 Prompt Resolution

All prompt-bearing config fields (identity.persona, identity.systemPrompt, identity.rules, identity.knowledge, heartbeat.prompt, cron.jobs.*.prompt, tools.*.skill) support a unified three-layer resolution system. Values are resolved at prompt build time, not at config parse time.

Resolution Layers

Values are checked in this order:

LayerDetectionBehavior
Layer 3: Code ModuleEnds with .ts or .jsDynamic import, call default(ctx) export, return result string.
Layer 1: File ReferenceStarts with ./ or /, or ends with .md or .txtRead file, then apply {{var}} template interpolation.
Layer 0: Inline PassthroughEverything elseReturn as-is, no transformation.

Layer 3 (code module) is checked first, so ./foo.ts is treated as a code module, not a file reference.

Template Interpolation ({{var}})

File-loaded content (Layer 1) supports {{key}} template interpolation using variables from identity.vars plus auto-populated values:

VariableSourceDescription
nameconfig.nameAgent instance name
versionconfig.versionConfig schema version
dateAuto-generatedCurrent ISO date (e.g., 2026-03-15)
(user-defined)identity.varsCustom key-value pairs from config

User-defined vars take precedence over auto-populated vars with the same name. Unknown {{key}} placeholders are left as-is.

Important: {{var}} interpolation only applies to file-loaded content (Layer 1). It does not apply to code module output (Layer 3) or inline YAML values (Layer 0). Inline YAML values already have ${ENV_VAR} substitution at config parse time.

Code Module Contract

Code modules (.ts/.js) must export a default function:

import type { PromptContext } from "@randal/core";

// For string fields (persona, systemPrompt, heartbeat.prompt, etc.)
export default function(ctx: PromptContext): string | Promise<string>;

// For rules arrays, modules may also return string[]
export default function(ctx: PromptContext): string | string[] | Promise<string | string[]>;

PromptContext Interface

interface PromptContext {
  basePath: string;              // Directory containing randal.config.yaml
  vars?: Record<string, string>; // Template variables (identity.vars + auto-populated)
  configName?: string;           // From config.name
}

Examples

Inline string (Layer 0):

identity:
  persona: "You are a helpful AI assistant."

File reference with template vars (Layer 1):

identity:
  persona: ./IDENTITY.md
  vars:
    name: my-agent
    company: Acme Corp

Where IDENTITY.md contains:

# {{name}}
You are {{name}}, built by {{company}}.

Code module (Layer 3):

identity:
  systemPrompt: ./instructions.ts

Where instructions.ts contains:

import type { PromptContext } from "@randal/core";

export default function(ctx: PromptContext): string {
  const isWeekend = [0, 6].includes(new Date().getDay());
  return isWeekend
    ? "Focus on maintenance tasks today."
    : "Prioritize active development tasks.";
}

Mixed rules array:

identity:
  rules:
    - "NEVER delete data"
    - ./safety-rules.md
    - ./dynamic-rules.ts

File entries are split by newlines into individual rules. Code modules may return string (split by newlines) or string[].


🪪 identity

Agent identity and knowledge configuration. Defaults to {} if omitted.

The primary source-machine persona authoring flow is bun run scripts/persona-preseed.ts, which launches an Randal native runner interview and writes both review artifacts and a Mac-mini-ready preseed bundle locally. randal init --persona remains available as a secondary local artifact writer.

The durable persona artifacts in that flow are:

  • .randal/identity/persona.identity.yaml: structured YAML identity bundle intended to be the source of truth
  • .randal/identity/PERSONA.md: readable persona document for human review
  • IDENTITY.md: rendered compatibility artifact for existing prompt-resolution flows
  • identity.ts: generated and manifest-tracked when the TypeScript identity compatibility path is selected

These generated files are tracked in .randal/managed-artifacts.yaml as Randal-managed outputs.

randal setup --preseed <file> accepts a trusted local wrapper bundle authored on a source machine and copied to the target Mac mini.

schemaVersion: "1"
kind: randal-preseed
trustedLocalOnly: true
authoring:
  sourceMachine: this-machine
  targetMachine: mac-mini
setup:
  configPath: randal.config.yaml
files:
  - scope: project
    path: randal.config.yaml
    kind: config
    content: |
      name: mac-mini-randal
      runner:
        workdir: .

Supported v1 keys:

  • schemaVersion: must be "1"
  • kind: must be randal-preseed
  • trustedLocalOnly: must be true
  • authoring.sourceMachine / authoring.targetMachine: optional handoff metadata for the source-machine -> target-machine workflow
  • setup.configPath: project-local config path to hand to the normal setup pipeline after materialization
  • setup.outputDir: optional Randal native runner output override
  • files[]: explicit file payloads to materialize before setup runs

Each files[] entry supports:

  • scope: project or home
  • path: relative path inside that scope
  • kind: artifact label recorded in .randal/managed-artifacts.yaml
  • content: plaintext file contents

Trusted-local-only note: plaintext secrets are supported in content, including .env payloads, but this format is intentionally not encrypted or remotely fetched in v1.

FieldTypeDefaultRequiredDescription
identity.personastringNoAgent persona injected into system prompt. Supports prompt resolution.
identity.systemPromptstringNoAdditional system instructions appended to prompt. Supports prompt resolution.
identity.knowledgestring[][]NoGlob patterns or file paths for knowledge files. Contents loaded and injected into system prompt. Supports prompt resolution.
identity.rulesstring[][]NoRules injected as a numbered list into system prompt. Each entry supports prompt resolution. File entries are split by newlines into individual rules.
identity.varsRecord<string, string>{}NoUser-defined template variables. Available as {{key}} in file-loaded prompts. Auto-populated vars: name, version, date.

🎯 runner

Agent execution configuration. The runner section is required.

Cutover note: runner.compaction.* was removed. The only supported execution model is a brain-managed session backed by plan files plus canonical .randal/loop-state.json state.

FieldTypeDefaultRequiredDescription
runner.defaultAgent"native" | "randal-native" | "mock""native"NoDefault native runner adapter.
runner.defaultModelstring"anthropic/claude-sonnet-4"NoDefault model identifier passed to the native runner (an OpenRouter catalog id — vendor prefixes like openai/… and anthropic/… all route through OpenRouter).
runner.effort.defaultLeveleco | pro | ultraproNoGlobal execution-effort default. It selects a versioned capability route and is independent from the account's billing plan. See Effort routing.
runner.effort.enabledbooleanfalse for legacy configsNoEnables effort recipe routing. The Settings effort control enables it when saved; omission preserves an upgraded installation's existing defaultModel.
runner.defaultMaxIterationsnumber20NoMaximum iterations per job before marking as failed.
runner.maxNativeToolTurnsnumber64NoMaximum native model/tool rounds per native run before Randal pauses with a resumable summary. Override per process with RANDAL_MAX_NATIVE_TOOL_TURNS for long local/account-auth jobs.
runner.maxDelegationDepthnumber8NoMaximum nested agent delegation depth. Progress/struggle guards remain responsible for stopping non-progressing loops.
runner.workdirstringYesWorking directory for agent processes.
runner.allowedWorkdirsstring[]NoAllowed working directories. If set, job workdirs are validated against this list before the agent is spawned. A job whose workdir is not the configured directory or one of its descendants is rejected with an error. Recommended for hosted and container deployments to restrict authenticated callers to the intended workspace.
runner.completionPromisestring"DONE"NoCompletion marker tag. The runner parses the configured value as <promise>{value}</promise> in agent output; the default is <promise>DONE</promise>.
runner.struggle.noChangeThresholdnumber3NoIterations with no file changes before the agent is considered stuck.
runner.struggle.maxRepeatedErrorsnumber3NoConsecutive non-zero exit codes before the agent is considered stuck.
runner.randalAuth.mode"disabled" | "openai-oauth""disabled"NoLegacy/debug Randal native runner auth-file materialization before native run. Native OpenAI account auth uses randal provider auth openai instead.
runner.randalAuth.authFileEnvstring"RANDAL_RUNTIME_AUTH_JSON"NoEnvironment variable name that contains the JSON payload written to the Randal native runner auth file at runtime.
runner.randalAuth.authFilePathstringRandal native runner default auth path under ~/.local/share/native/auth.jsonNoOptional override for where the runner writes the materialized auth file. Leave unset unless your runtime needs a different location.
runner.randal.variantstringNoOptional Randal native runner CLI variant flag for explicit legacy/debug Randal native runner runs.

Loop-State Cutover

  • .randal/loop-state.json is the canonical durable build-state file.
  • Build entries are keyed by plan slug, not by transient runner job ID.
  • Corrupted loop-state files now raise explicit errors instead of being silently reset.
  • Legacy runner-written statuses such as active, completed, and errored are no longer valid in canonical loop-state.

Provider Credential Example

OpenRouter is the only supported LLM provider — vendor-prefixed model ids (openai/…, anthropic/…) are OpenRouter catalog ids and all route through the one credential:

runner:
  defaultAgent: native
  defaultModel: openai/gpt-5.5
  workdir: /app/workspace
  providerAuth:
    defaultProvider: openai
    providers:
      openai:
        model: openai/gpt-5.5
      anthropic:
        model: anthropic/claude-fable-5
    credentials:
      openrouter: OPENROUTER_API_KEY
credentials:
  allow: [OPENROUTER_API_KEY]
  inherit: [PATH, HOME, USER, SHELL, TERM]

Account-auth modes (Claude Max, ChatGPT/GPT Pro) and direct OPENAI_API_KEY/ANTHROPIC_API_KEY provider routing were removed; legacy config keys for them are ignored with a startup warning. Managed plans need no provider key at all — inference routes through the Randal metering proxy with the user's JWT.


🔐 credentials

Environment and secret management. Defaults to {} if omitted.

FieldTypeDefaultRequiredDescription
credentials.envFilestring"./.env"NoPath to .env file (relative to config file).
credentials.allowstring[][]NoAllowlist of variable names to load from the .env file. Only listed vars are passed to the agent.
credentials.inheritstring[]["PATH", "HOME", "SHELL", "TERM"]NoEnvironment variables inherited from the parent process.

🧩 connectors

Optional agent tool connectors. Defaults to {} if omitted. Built-in manifests currently include image, video, and notion under connectors/builtin/.

Connector config stores only non-secret state. Raw connector secrets are stored by the configured secret provider and injected only into spawned brain process environments.

FieldTypeDefaultRequiredDescription
connectors.<id>.sourcestringbuiltin:<id>NoConnector manifest source. Built-in sources must match the map key, e.g. connectors.notion.source: builtin:notion.
connectors.<id>.enabledbooleanfalseNoWhether new brain sessions should receive this connector's MCP/tools.
connectors.<id>.secretScopeuser | workspacesecrets.defaultScopeNoIntended secret ownership scope. Local v1 stores through the local user provider.
connectors.<id>.settingsobject{}NoNon-secret connector settings validated against the manifest schema.

Example:

connectors:
  notion:
    source: builtin:notion
    enabled: true
    secretScope: user
    settings:
      defaultSearchDepth: basic

Connector changes made through the gateway keep randal serve running. New sessions load the updated connector config; active brain sessions may be interrupted/reloaded so they can see updated tools.

🔑 secrets

Connector secret-provider configuration. Defaults to the local user env-file provider.

FieldTypeDefaultRequiredDescription
secrets.providerlocal-user-envlocal-user-envNoSecret provider implementation.
secrets.defaultScopeuser | workspaceuserNoDefault connector secret scope.
secrets.processEnv.connectors.<connectorId>.<SECRET_NAME>env var nameNoExplicit process env mapping for connector secrets. Values must be uppercase env identifiers and are env var names only, not raw secrets.
secrets.localUserEnv.workspaceIdstringgeneratedNoStable workspace id used in the local env-file path.
secrets.localUserEnv.workspaceIndexPathstring~/.randal/workspaces/index.jsonNoWorkspace id index. Must stay under ~/.randal unless unsafe paths are explicitly allowed.
secrets.localUserEnv.envFilePatternstring~/.randal/secrets/workspaces/{workspaceId}.envNoLocal user secret file pattern.
secrets.localUserEnv.projectEnvFallbackbooleantrueNoAllow scoped fallback reads from the project .env.
secrets.localUserEnv.allowUnscopedProjectEnvFallbackbooleanfalseNoAllow legacy unscoped fallback names such as NOTION_API_KEY. Prefer scoped keys.
secrets.localUserEnv.allowUnsafePathsbooleanfalseNoAllow secret files outside ~/.randal; intended only for tests and controlled migrations.

Scoped project .env fallback keys use <CONNECTOR_ID>__<SECRET_NAME>, for example NOTION__NOTION_API_KEY. Explicit process env mappings are resolved after scoped local/project files and before optional unscoped fallback keys. See Connectors and Secret Providers for the guided setup and storage model.


🔗 services

Named external service bindings with declarative credential delivery. Defaults to {} if omitted.

Each service is a named entry with a credential delivery mechanism and optional audit logging.

FieldTypeDefaultRequiredDescription
services.<name>.descriptionstringNoHuman-readable purpose of this service.
services.<name>.credentials.type"env" | "file" | "ambient" | "script" | "none"YesCredential delivery mechanism.
services.<name>.auditbooleanfalseNoLog when agent spawns with this service's credentials.

Credential Types

type: env — Inject environment variables directly.

FieldTypeDescription
varsRecord<string, string>Key-value map of env vars to inject. Supports ${ENV_VAR} substitution.

type: file — Copy a credential file and set env vars pointing to it.

FieldTypeDescription
filestringSource file path (relative to config).
mountAsstringDestination path where the file is copied.
varsRecord<string, string>Env vars to inject (typically pointing to the mounted file).

type: ambient — Keep existing host binaries and config dirs available.

FieldTypeDescription
binariesstring[]Binary names to ensure stay in PATH.
pathsstring[]Config directories the agent needs access to.

type: script — Run a script before each job and capture output as credentials.

FieldTypeDescription
commandstringScript to execute (relative to config).
varsRecord<string, string>Map of var names. Use "stdout" as the value to capture script output.
ttlnumberRe-run interval in seconds. Cached until expired.

type: none — Explicitly block access to a service.

FieldTypeDescription
binariesstring[]Binary names to strip from PATH.
varsstring[]Env var names to remove from the child process environment.

Example:

services:
  github:
    description: "GitHub via provisioned PAT"
    credentials:
      type: env
      vars:
        GH_TOKEN: ${GITHUB_PAT}
        GITHUB_TOKEN: ${GITHUB_PAT}
    audit: true

  # GitHub/GH CLI access is a core git-ops service binding today, not a
  # manifest connector. Use credentials/services for branch, PR, and checks work.

  gcloud:
    description: "Google Cloud via service account"
    credentials:
      type: file
      file: ./secrets/gcp-sa-key.json
      mountAs: /tmp/gcp-creds.json
      vars:
        GOOGLE_APPLICATION_CREDENTIALS: /tmp/gcp-creds.json

  aws:
    description: "AWS explicitly blocked"
    credentials:
      type: none
      binaries: [aws, aws-vault]
      vars: [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN]

  internal-api:
    description: "Internal API via rotating token"
    credentials:
      type: script
      command: ./scripts/get-api-token.sh
      vars:
        INTERNAL_API_TOKEN: stdout
      ttl: 3600

🔒 sandbox

Process isolation configuration. Controls how aggressively Randal restricts the agent child process environment. Defaults to { enforcement: "none" } (current behavior preserved).

FieldTypeDefaultDescription
sandbox.enforcement"none" | "env-scrub""none"Enforcement level. none = no restrictions. env-scrub = apply PATH filtering, home access restrictions, and env scrubbing.
sandbox.pathFilter.mode"inherit" | "allowlist" | "blocklist""inherit"How to filter the PATH variable.
sandbox.pathFilter.allowstring[][]PATH prefixes to keep (when mode is allowlist). Supports ~ expansion.
sandbox.pathFilter.blockstring[][]Binary names whose containing dirs are removed (when mode is blocklist).
sandbox.homeAccess.sshbooleantrueAllow ~/.ssh access. When false: sets GIT_SSH_COMMAND=/bin/false, unsets SSH_AUTH_SOCK.
sandbox.homeAccess.gitconfigbooleantrueAllow ~/.gitconfig credential helpers. When false: sets GIT_CONFIG_GLOBAL=/dev/null.
sandbox.homeAccess.dockerbooleantrueAllow ~/.docker/config.json. When false: sets DOCKER_CONFIG=/dev/null.
sandbox.homeAccess.awsbooleantrueAllow ~/.aws. When false: unsets all AWS_* vars, sets null config paths.

When any homeAccess flag is false, a temporary HOME directory is created with only the allowed config dirs symlinked in. The temp dir is cleaned up after the job completes.

Example:

sandbox:
  enforcement: env-scrub
  pathFilter:
    mode: allowlist
    allow: [/usr/bin, /usr/local/bin, ~/.bun/bin]
  homeAccess:
    ssh: false
    gitconfig: false
    aws: false

⬆️ updates

Self-update configuration. Defaults to { autoCheck: false, channel: "stable" }.

FieldTypeDefaultDescription
updates.autoCheckbooleanfalseCheck for updates on randal serve startup.
updates.channel"stable" | "latest""stable"stable = follow semver tags. latest = follow main HEAD.

Example:

updates:
  autoCheck: true
  channel: stable

📡 gateway

Server and channel configuration. Defaults to {} if omitted.

FieldTypeDefaultRequiredDescription
gateway.channelsChannel[][]NoCommunication channels. At least one HTTP channel is needed for randal serve.

🌐 HTTP Channel

FieldTypeDefaultRequiredDescription
type"http"YesChannel type discriminator.
portnumber7600NoHTTP server port.
hoststring127.0.0.1Hosted: YesInterface to bind. Omitted → loopback, so the embedded desktop app never exposes the agent API to the local network. Public deployments (Railway/Docker/reverse-proxied) must set "0.0.0.0" here (or via RANDAL_HOST/HOST env). On a loopback bind the gateway also enforces a Host-header allowlist (DNS-rebinding defense); see SECURITY.md.
authstringYesBearer token for API authentication. Use ${RANDAL_API_TOKEN} to read from env.
corsOriginstringHosted: YesExplicit browser origin allowed to call the authenticated HTTP API, for example https://operator.example.com. When omitted on authenticated HTTP channels, Randal does not silently emit wildcard CORS for protected API responses.
allowQueryTokenAuthbooleanfalseNoTemporary compatibility escape hatch for legacy ?token= bearer auth. Enabling this logs a warning because URLs can leak through browser history, proxy logs, referer headers, and telemetry. Prefer Authorization: Bearer ... headers or the /auth/session cookie flow.

Hosted HTTP Security Posture

Local development can keep using local-only tokens and same-origin browser access. Hosted deployments should fail closed by configuration:

  • set auth: "${RANDAL_API_TOKEN}" with no fallback default
  • set corsOrigin: "${RANDAL_CORS_ORIGIN}" to the exact trusted browser origin
  • set runner.allowedWorkdirs to the mounted workspace directory
  • keep allowQueryTokenAuth unset unless a legacy client migration requires it temporarily
runner:
  workdir: /data/randal/workspace
  allowedWorkdirs:
    - /data/randal/workspace

gateway:
  channels:
    - type: http
      port: 7600
      auth: "${RANDAL_API_TOKEN}"
      corsOrigin: "${RANDAL_CORS_ORIGIN}"

Built-In And Legacy Channel Types

Randal Console and custom relays should use the HTTP channel and /api/sessions/*. Some named adapter config schemas remain in the codebase for compatibility, but they are not the recommended first path for new product surfaces.

Legacy Named Chat Adapter Example

FieldTypeDefaultRequiredDescription
type"discord"YesChannel type discriminator.
tokenstringYesDiscord bot token.
allowFromstring[]NoDiscord user ID allowlist.
guildIdstringNoGuild ID for instant slash command registration. If omitted, uses global registration (~1 hour propagation).
serversDiscordServer[][]NoPer-server configuration. See Discord Guide.

Prefer the Custom Relay Guide for new integrations.

🎙️ Voice Channel

FieldTypeDefaultRequiredDescription
type"voice"YesChannel type discriminator.
allowFromstring[][]NoLegacy trusted admin caller list. Preserved as an admin-trust input for PSTN callers.
access.trustedCallersstring[][]NoAdditional trusted admin caller E.164 numbers.
access.unknownInbound"deny" | "external""deny"NoUnknown inbound caller handling. deny is the secure default.
access.defaultExternalGrantsstring[][]NoDefault explicit grants for external voice sessions when no per-session override is supplied.

Other Legacy Channel Schemas

Additional named channel schemas exist in packages/core/src/config.ts for compatibility and experiments. For new external products, keep product-specific SDKs in a custom relay and connect through the HTTP session API.


🧠 memory

Memory system configuration. Defaults to {} if omitted.

FieldTypeDefaultRequiredDescription
memory.store"postgres""postgres"NoStorage backend. Postgres is the only supported runtime store.
memory.indexstring"memory-{name}"NoLogical memory namespace retained for compatibility with existing config shape.
memory.autoInject.enabledbooleantrueNoAuto-inject relevant memory into agent system prompt.
memory.autoInject.maxResultsnumber5NoMaximum memory results to inject per iteration.
memory.sharing.publishTostringNoShared logical scope for cross-agent learnings. Backed by tenant-scoped Postgres rows.
memory.sharing.readFromstring[][]NoShared logical scopes to read from. Backed by tenant-scoped Postgres rows.

🗄️ database

database configures the Postgres source of truth. DATABASE_URL or RANDAL_DATABASE_URL may supply the URL at runtime.

FieldTypeDefaultRequiredDescription
database.profile"local" | "generic" | "supabase" | "railway""local"NoRuntime profile. Core SQL remains portable; Supabase overlays are optional.
database.urlstring${DATABASE_URL}Hosted: YesPostgres URL. Keep secret; diagnostics redact credentials.
database.schemastring"public"NoSchema containing Randal tables.
database.ssl"disable" | "require"profile-dependentNoUse require for most managed providers.
database.migrationMode"manual" | "startup""manual"NoOperators should prefer explicit migrations before deploy.
database.orgIdUUIDlocal singletonNoTenant org scope for local or cloud identity.
database.projectIdUUIDlocal singletonNoTenant project/workspace scope.
database.pool.minnumber0NoMinimum pool size.
database.pool.maxnumber10NoMaximum runtime connections. Tune for provider limits.
database.pool.idleTimeoutMillisnumber30000NoIdle connection lifetime.
database.pool.connectionTimeoutMillisnumber5000NoConnection acquisition timeout.

Local singleton example:

memory:
  store: postgres
database:
  profile: local
  url: "${DATABASE_URL}"
  ssl: disable
  orgId: "00000000-0000-4000-8000-000000000001"
  projectId: "00000000-0000-4000-8000-000000000001"

Cloud tenant example:

memory:
  store: postgres
database:
  profile: generic
  url: "${DATABASE_URL}"
  ssl: require
  migrationMode: manual
  orgId: "${RANDAL_ORG_ID}"
  projectId: "${RANDAL_PROJECT_ID}"

Artifact metadata, redaction status, retention class, and checksums live in Postgres. External payload stores must be protected and copied separately when restoring or moving databases. See Postgres DAG Operations for runbooks.

🧹 Retention Reaper (env knobs)

The gateway runs a periodic retention reaper that deletes expired runner_stream_events (TTL-based) and ages out job_events, tool_invocations, checkpoints, and artifacts rows in bounded batches. Guardrails: rows classed audit/legal-hold are never touched (including transitively — artifacts referenced by a held observer capture row are kept), job-scoped rows are only reaped once their job is terminal, and artifacts with a cloud object location are kept until object-store deletion exists. It sweeps once shortly after startup, then on an interval. Tuned via environment variables (not config fields):

Env varDefaultDescription
RANDAL_RETENTION_DISABLEDunsetSet to 1 to disable the reaper entirely (e.g. audit/compliance holds beyond retention classes).
RANDAL_RETENTION_SWEEP_INTERVAL_MS21600000 (6 h)Interval between sweeps.
RANDAL_RETENTION_INITIAL_DELAY_MS120000 (2 min)Delay before the startup sweep — short-lived desktop sidecar sessions still reap without waiting for the first interval tick.
RANDAL_RETENTION_EPHEMERAL_DAYS7Max age for rows classed ephemeral.
RANDAL_RETENTION_STANDARD_DAYS90Max age for rows classed standard (and unclassed tables like checkpoints/tool_invocations).
RANDAL_RETENTION_BATCH_SIZE1000Rows per DELETE batch (bounded so the first sweep on an old install can't hold long table locks).

Invalid values fall back to the defaults. Deletes are rows-only; on-disk observer capture files are cleaned by the desktop app's separate capture-dir sweep.

The held-row exclusion is also enforced by the database itself (migration 019_retention_delete_guards.sql): job_events/runner_stream_events remain append-only for UPDATEs, deletes of audit/legal-hold rows are rejected by trigger, and stream events cannot be deleted before their ttl_expires_at — even by a misbehaving client with write access.

🔌 Embedder Configuration

Nested under memory.embedder. Defaults to { type: "builtin" }.

Builtin (default, no configuration needed):

memory:
  embedder:
    type: builtin

OpenAI:

FieldTypeDefaultRequired
type"openai"Yes
modelstring"text-embedding-3-large"No
apiKeystringYes

OpenRouter:

FieldTypeDefaultRequired
type"openrouter"Yes
modelstringYes
apiKeystringYes

Randal-managed OpenRouter traffic uses canonical app attribution headers (HTTP-Referer: https://randal.bot, X-Title: Randal) for direct requests and generated Randal native runner provider configuration. This attribution is intentionally not exposed as a normal user-facing override.

Ollama:

FieldTypeDefaultRequired
type"ollama"Yes
modelstringYes
urlstring"http://localhost:11434"No

🛠️ tools

Array of external tool definitions. Defaults to [].

FieldTypeDefaultRequiredDescription
tools[].namestringYesTool name.
tools[].binarystringYesExecutable binary name (must be on PATH).
tools[].skillstringNoPath to a skill documentation markdown file.
tools[].platforms("darwin" | "linux" | "win32")[]["darwin", "linux"]NoPlatforms where this tool is available.

💰 tracking

Cost tracking configuration. Defaults to {}.

FieldTypeDefaultRequiredDescription
tracking.tokenPricingRecord<string, { input: number, output: number }>{}NoPer-model token pricing in dollars per million tokens. Keys are model identifiers. Example: Claude Sonnet at $3/M input, $15/M output → input: 3.00, output: 15.00.

💓 heartbeat

The heartbeat is a periodic "check in and use your judgment" primitive. The agent wakes at a configured interval, reads a prompt/checklist, and decides whether anything needs attention.

FieldTypeDefaultDescription
heartbeat.enabledbooleanfalseEnable periodic heartbeat.
heartbeat.everystring"30m"Interval between ticks. Supports: "15m", "1h", "2h30m".
heartbeat.promptstring"./HEARTBEAT.md"Path to heartbeat prompt file, or inline prompt string.
heartbeat.activeHours.startstringStart of active window (HH:MM format).
heartbeat.activeHours.endstringEnd of active window (HH:MM format).
heartbeat.activeHours.timezonestring"UTC"Timezone for active hours.
heartbeat.targetstring"none"Where to send heartbeat results.
heartbeat.modelstringOverride model for heartbeat (use a cheap model like claude-haiku-4).

Example:

heartbeat:
  enabled: true
  every: 30m
  prompt: ./HEARTBEAT.md
  activeHours:
    start: "08:00"
    end: "22:00"
    timezone: "America/Denver"
  model: anthropic/claude-haiku-4

📅 cron

Precise scheduled tasks with three schedule formats.

FieldTypeDefaultDescription
cron.jobs.<name>.schedulestring | {every} | {at}Cron expression, interval, or one-shot time.
cron.jobs.<name>.promptstringThe prompt to execute.
cron.jobs.<name>.execution"main" | "isolated""isolated"main: queue for next heartbeat. isolated: run as standalone job.
cron.jobs.<name>.modelstringOverride model for this job.
cron.jobs.<name>.efforteco | pro | ultraPin the scheduled run's effort independently from its owner's later account-default changes. An explicit model still wins route selection.
cron.jobs.<name>.announcebooleanfalseWhether to announce results to channels.

Schedule formats:

  • Cron expression: "0 7 * * *" (5-field: minute hour day-of-month month day-of-week)
  • 🔄 Interval: { every: "30m" } (repeating interval)
  • 🎯 One-shot: { at: "2026-03-15T14:00:00Z" } (fires once, then marked completed)

Example:

cron:
  jobs:
    morning-briefing:
      schedule: "0 8 * * *"
      prompt: "Review pending tasks. Compile a morning status."
      execution: isolated
      announce: true
    periodic-check:
      schedule: { every: "1h" }
      prompt: "Check system health."
      execution: main
    deploy-reminder:
      schedule: { at: "2026-03-15T14:00:00Z" }
      prompt: "Remind the team about the deployment."
      execution: isolated

🪝 hooks

External event triggers via HTTP webhooks.

FieldTypeDefaultDescription
hooks.enabledbooleanfalseEnable webhook endpoints.
hooks.tokenstringAuth token for webhook requests. If not set, hooks are disabled.
hooks.pathstring"/hooks"URL path prefix for hook endpoints.

When enabled, two endpoints are mounted:

  • POST /hooks/wake — Wake the agent with a message. Modes: "now" (immediate heartbeat) or "next-heartbeat" (queued).
  • POST /hooks/agent — Submit a job or queue a message. Modes: "now" (isolated job) or "next-heartbeat" (queued).

All requests require Authorization: Bearer <token> or x-randal-token: <token>.

Example:

hooks:
  enabled: true
  token: "${RANDAL_HOOK_TOKEN}"
# Trigger immediate heartbeat with context
curl -X POST http://localhost:7600/hooks/wake \
  -H "Authorization: Bearer $RANDAL_HOOK_TOKEN" \
  -d '{"text": "New VIP email received", "mode": "now"}'

# Queue for next heartbeat
curl -X POST http://localhost:7600/hooks/wake \
  -H "Authorization: Bearer $RANDAL_HOOK_TOKEN" \
  -d '{"text": "Low priority notification", "mode": "next-heartbeat"}'

📁 Example Configs

🏁 Minimal (local one-shot)

name: my-agent
runner:
  workdir: ~/dev/my-project

💻 Personal Dev Agent

name: dev-agent
identity:
  persona: "Senior TypeScript engineer"
  rules:
    - "Write tests for all new functions"
    - "Use Biome for formatting"
runner:
  defaultAgent: native
  defaultModel: anthropic/claude-sonnet-4
  defaultMaxIterations: 30
  workdir: ~/dev/my-project
credentials:
  envFile: ./.env
  allow: [OPENROUTER_API_KEY]
gateway:
  channels:
    - type: http
      port: 7600
      auth: "${RANDAL_API_TOKEN}"
memory:
  store: postgres
database:
  url: "${DATABASE_URL}"

🏭 Production Agent with Postgres

name: support-agent
posse: production
identity:
  persona: "Customer support specialist"
  knowledge:
    - ./knowledge/help-center/*.md
    - ./knowledge/faq.md
  rules:
    - "Never delete production data"
    - "Never expose PII"
    - "Always escalate payment issues"
runner:
  defaultAgent: native
  defaultModel: claude-sonnet-4
  workdir: /home/node/workspace
credentials:
  envFile: ./.env
  allow: [OPENROUTER_API_KEY, SUPABASE_URL, SUPABASE_AGENT_KEY]
gateway:
  channels:
    - type: http
      port: 7600
      auth: "${RANDAL_API_TOKEN}"
memory:
  store: postgres
  sharing:
    publishTo: shared
    readFrom: [shared]
database:
  profile: generic
  url: "${DATABASE_URL}"
  ssl: require
  migrationMode: manual

🛠️ Multi-Tool Agent

name: ops-agent
runner:
  defaultAgent: native
  workdir: ~/dev/infra
credentials:
  envFile: ./.env
  allow: [OPENROUTER_API_KEY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY]
  inherit: [PATH, HOME, SHELL, TERM, SSH_AUTH_SOCK]
gateway:
  channels:
    - type: http
      port: 7600
      auth: "${RANDAL_API_TOKEN}"
tools:
  - name: terraform
    binary: terraform
    skill: ./skills/terraform.md
    platforms: [darwin, linux]
  - name: kubectl
    binary: kubectl
    platforms: [darwin, linux]
tracking:
  tokenPricing:
    "anthropic/claude-sonnet-4":
      input: 0.000003
      output: 0.000015

💬 Personal Assistant with Console

name: assistant
identity:
  persona: "Personal dev assistant reachable through Randal Console and custom relays."
runner:
  defaultAgent: native
  defaultModel: claude-sonnet-4
  workdir: ~/dev
credentials:
  envFile: ./.env
  allow: [OPENROUTER_API_KEY]
  inherit: [PATH, HOME, SHELL, TERM]
gateway:
  channels:
    - type: http
      port: 7600
      auth: "${RANDAL_API_TOKEN}"
memory:
  store: postgres
  autoInject:
    enabled: true
    maxResults: 5
database:
  url: "${DATABASE_URL}"

🎙️ Voice & Video

Voice calling is currently unavailable. Live-call speech-to-text was removed pending the billing-integrated voice rework, so getVoiceCapability reports voice unavailable even when everything below is configured. The scaffolding config stays for when voice returns. Legacy voice.stt.provider / voice.stt.model / voice.stt.apiKey keys (and voice.tts.provider: openai) parse with a startup warning and are ignored.

FieldTypeDefaultDescription
voice.enabledbooleanfalseEnable voice/video features
voice.livekit.urlstring""LiveKit server URL
voice.livekit.apiKeystring""LiveKit API key
voice.livekit.apiSecretstring""LiveKit API secret
voice.twilio.accountSidstring""Twilio Account SID
voice.twilio.authTokenstring""Twilio Auth Token
voice.twilio.phoneNumberstring""Twilio phone number
voice.stt.openrouterModelstringAudio-capable OpenRouter chat model that transcribes teach-narration audio (default google/gemini-2.5-flash)
voice.tts.provider"elevenlabs" | "cartesia" | "edge""elevenlabs"Text-to-speech provider
voice.tts.voicestringTTS voice ID
voice.tts.apiKeystring""TTS provider API key
voice.turnDetection.mode"auto" | "manual""auto"Turn detection mode
voice.video.enabledbooleanfalseEnable video features
voice.video.visionModelstring"gpt-4o"Vision model for processing screen shares
voice.video.publishScreenbooleanfalsePublish agent screen as video track
voice.video.recordSessionsbooleanfalseRecord voice/video sessions
voice.video.recordPathstring"./recordings"Path to save recordings

Browser/media voice scaffolding requires LiveKit + TTS configuration. PSTN calling requires Twilio in addition to that browser/media baseline. Neither can carry live calls until the voice rework restores speech-to-text.


🌐 Multi-Instance Mesh

FieldTypeDefaultDescription
mesh.enabledbooleanfalseEnable mesh orchestration
mesh.rolestring (enum)Agent's broad domain. One of: product-engineering, platform-infrastructure, security-compliance, data-intelligence, design-experience, content-communications, revenue-growth, customer-operations, strategy-finance, legal-governance. Used for routing pre-filter and analytics.
mesh.expertisestring | {file, additional?}Rich natural language expertise description. Supports inline string, file reference ({ file: "./profile.md" }), or combined ({ file: "./profile.md", additional: "Also knows X" }). Used for semantic routing when embeddings are available.
mesh.endpointstringThis instance's HTTP endpoint for peer communication
mesh.routingWeights.expertisenumber0.4Weight for semantic expertise matching in routing (2-tier: vector cosine → role match)
mesh.routingWeights.reliabilitynumber0.3Weight for reliability score in routing
mesh.routingWeights.loadnumber0.2Weight for current load in routing
mesh.routingWeights.modelMatchnumber0.1Weight for model availability in routing

📊 Analytics & Self-Learning

FieldTypeDefaultDescription
analytics.enabledbooleantrueEnable analytics engine. Set to false to opt out.
analytics.autoAnnotationPromptbooleantruePrompt for annotations after job completion
analytics.feedbackInjectionbooleantrueInject empirical guidance into system prompts
analytics.recommendationFrequency"daily" | "weekly" | "on-demand""on-demand"How often to generate recommendations
analytics.domainKeywordsRecord<string, string[]>(see defaults)Custom keyword-to-domain mapping. Default domains: product-engineering, platform-infrastructure, security-compliance, data-intelligence, design-experience, content-communications, revenue-growth, customer-operations, strategy-finance, legal-governance. Override or extend per domain.
analytics.agingHalfLifenumber30Half-life in days for annotation aging

Analytics is default-on and stores annotations in the configured Postgres source of truth. In normal gateway mode, annotation-store initialization failure should degrade analytics with a visible warning/status rather than preventing startup. Inspect live data with randal analytics scores, randal analytics recommendations, and the analytics HTTP routes.


🌍 Browser Automation

FieldTypeDefaultDescription
browser.enabledbooleanfalseEnable browser automation
browser.headlessbooleantrueRun browser in headless mode
browser.profileDirstringDirectory for browser profile persistence
browser.sandboxbooleanfalseRun browser in sandbox container
browser.viewport.widthnumber1280Browser viewport width
browser.viewport.heightnumber720Browser viewport height
browser.timeoutnumber30000Default timeout in milliseconds

Randal's web_search / web_extract native tools run entirely in-process against keyless public sources — no API key, no per-query fees. See web-search.md for engine details, User-Agent policy, and per-topology expectations.

FieldTypeDefaultDescription
webSearch.enabledbooleantrueExpose the web_search/web_extract tools. Turning this off (or back on) applies to new model turns without a restart — but if it was off at startup, turning it on requires a restart.
webSearch.contactstringContact URL or email appended to the honest User-Agent sent to API engines (Wikipedia, HN, Marginalia, GDELT, RSS).
webSearch.marginaliaApiKeystringMarginalia key (free by email); supports ${MARGINALIA_API_KEY} substitution. Omitted → the documented public demo key.
webSearch.engines.duckduckgobooleantruePrimary general-web engine (HTML endpoint, POST).
webSearch.engines.wikipediabooleantrueEntity/reference lane (MediaWiki API).
webSearch.engines.hackernewsbooleantrueDev lane (HN Algolia API).
webSearch.engines.marginaliabooleantrueLong-tail/indie-web lane.
webSearch.engines.mojeekbooleanfalseIndependent index; its robots.txt disallows /search, so this ships opt-in.
webSearch.engines.bingNewsRssbooleantruePrimary news-URL source (RSS).
webSearch.engines.googleNewsRssbooleantrueNews headline/recency signal only — its article links are not resolvable, so they never appear as results.
webSearch.engines.gdeltbooleantrueGlobal news coverage (GDELT DOC API).
webSearch.timeoutMsnumber10000Per-engine search request timeout.
webSearch.extract.timeoutMsnumber15000Per-URL extraction fetch timeout.
webSearch.extract.maxUrlsnumber10Max URLs per web_extract call.
webSearch.extract.renderJavaScriptbooleantrueRe-read a page with a headless browser (the system's Chrome, over CDP) when the static fetch finds no readable content — SPA shells, client-rendered listings and galleries. Without Chrome installed, extraction fails honestly instead.
webSearch.extract.renderTimeoutMsnumber20000Timeout for a JavaScript-rendered page read.
webSearch.cache.ttlSecondsnumber1800Search result cache TTL (general topic).
webSearch.cache.newsTtlSecondsnumber600Search result cache TTL (news topic).
webSearch.cache.maxEntriesnumber500LRU cache size.

🔄 Runner Extensions

MCP Server

FieldTypeDefaultDescription
runner.mcpServer.enabledbooleanfalseEnable MCP server for bidirectional agent communication
runner.mcpServer.portnumber7601MCP server port
runner.mcpServer.toolsstring[]["memory_search", "context", "status", "skills", "annotate"]Tools to expose

💬 Additional Channel Types

The gateway has additional named channel schemas for compatibility and experiments. These are not the recommended path for new product surfaces; prefer a custom relay over /api/sessions/* unless you are maintaining existing adapter code.

Available community channel types: telegram, slack, email, whatsapp, signal, voice. Config schemas remain in packages/core/src/config.ts for reference.

Native provider and connector auth

runner.defaultAgent now defaults to native; native is retained only for explicit legacy/debug compatibility. runner.providerAuth.defaultProvider defaults to openrouter and selects which per-vendor default model new runs use; routing is always OpenRouter with the openrouter credential. Native chat stream events show the selected provider, auth mode, model, and redacted credential reference. Account-auth modes (Claude Max, GPT Pro) were removed.

Connector auth blocks use mode, secretRef, scopes, expiration, revocation, and permission-prompt metadata. Store raw connector tokens only in the secret provider; do not put them in config, job metadata, runner events, or tool logs.