docs/config-reference.md

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).


📋 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 Tavily, 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 Tavily, 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 OpenCode 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 OpenCode 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 .opencode/loop-state.json state.

FieldTypeDefaultRequiredDescription
runner.defaultAgent"opencode" | "mock""opencode"NoDefault agent adapter.
runner.defaultModelstring"anthropic/claude-sonnet-4"NoDefault model identifier passed to the agent CLI. For Railway GPT Pro / OpenCode OAuth deployments, set this to openai/gpt-5.4.
runner.defaultMaxIterationsnumber20NoMaximum iterations per job before marking as failed.
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.opencodeAuth.mode"disabled" | "openai-oauth""disabled"NoEnables OpenCode auth-file materialization before opencode run. Use openai-oauth for the Railway subscription-auth flow.
runner.opencodeAuth.authFileEnvstring"OPENCODE_AUTH_JSON"NoEnvironment variable name that contains the JSON payload written to the OpenCode auth file at runtime.
runner.opencodeAuth.authFilePathstringOpenCode default auth path under ~/.local/share/opencode/auth.jsonNoOptional override for where the runner writes the materialized auth file. Leave unset unless your runtime needs a different location.
runner.opencode.variantstringNoOptional OpenCode CLI variant flag. For GPT Pro Railway usage, set xhigh.

Loop-State Cutover

  • .opencode/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.

OpenCode OAuth Example

Use this when you want a headless Railway deploy to reuse a one-time local OpenCode login:

runner:
  defaultAgent: opencode
  defaultModel: openai/gpt-5.4
  workdir: /app/workspace
  opencodeAuth:
    mode: openai-oauth
    authFileEnv: OPENCODE_AUTH_JSON
  opencode:
    variant: xhigh

Bootstrap flow:

  1. Run opencode auth login locally.
  2. Copy ~/.local/share/opencode/auth.json into a secret named OPENCODE_AUTH_JSON.
  3. Start Railway normally; the runner materializes the auth file before spawning OpenCode.

Security handling:

  • Treat OPENCODE_AUTH_JSON like a credential.
  • Store it in secret management only.
  • Never commit it to the repo or paste it into issue threads.
  • Avoid logging the JSON payload or writing ad hoc debug copies of the file.

Alternatives remain supported: API-key flows via OPENROUTER_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY still work without runner.opencodeAuth.*.


🔐 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 tavily, image, and video 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.tavily.source: builtin:tavily.
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:
  tavily:
    source: builtin:tavily
    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 TAVILY_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 TAVILY__TAVILY_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.
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.

🔌 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 OpenCode 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>.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: opencode
  defaultModel: anthropic/claude-sonnet-4
  defaultMaxIterations: 30
  workdir: ~/dev/my-project
credentials:
  envFile: ./.env
  allow: [ANTHROPIC_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: opencode
  defaultModel: claude-sonnet-4
  workdir: /home/node/workspace
credentials:
  envFile: ./.env
  allow: [ANTHROPIC_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: opencode
  workdir: ~/dev/infra
credentials:
  envFile: ./.env
  allow: [ANTHROPIC_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: opencode
  defaultModel: claude-sonnet-4
  workdir: ~/dev
credentials:
  envFile: ./.env
  allow: [ANTHROPIC_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

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.provider"deepgram" | "whisper" | "assemblyai""deepgram"Speech-to-text provider
voice.stt.modelstringSTT model name
voice.stt.apiKeystring""STT provider API key
voice.tts.provider"elevenlabs" | "cartesia" | "openai" | "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 requires LiveKit + STT + TTS configuration. PSTN calling requires Twilio in addition to that browser/media baseline.


🌐 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

🔄 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.