⚙️ Configuration Reference
Randal is configured via a YAML file. Accepted filenames (checked in order):
randal.config.yamlrandal.config.ymlrandal.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
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
name | string | — | Yes | Instance name. Used for memory index naming and identification. |
version | string | "0.1" | No | Config schema version. |
posse | string | — | No | Team/group identifier for multi-agent coordination. |
managed.bootstrap | object | — | No | Optional published-artifact bootstrap metadata. Used only for runtime startup, never as Config Studio state. |
capabilities | string[] | [] | No | Core/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.bootstrapis 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.
| Capability | MCP Server(s) | Description |
|---|---|---|
memory | memory | Persistent memory/chat/DAG access backed by Postgres. Also enabled automatically when memory.store is configured. |
scheduler | scheduler | Heartbeat 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:
| Layer | Detection | Behavior |
|---|---|---|
| Layer 3: Code Module | Ends with .ts or .js | Dynamic import, call default(ctx) export, return result string. |
| Layer 1: File Reference | Starts with ./ or /, or ends with .md or .txt | Read file, then apply {{var}} template interpolation. |
| Layer 0: Inline Passthrough | Everything else | Return 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:
| Variable | Source | Description |
|---|---|---|
name | config.name | Agent instance name |
version | config.version | Config schema version |
date | Auto-generated | Current ISO date (e.g., 2026-03-15) |
| (user-defined) | identity.vars | Custom 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 reviewIDENTITY.md: rendered compatibility artifact for existing prompt-resolution flowsidentity.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 berandal-preseedtrustedLocalOnly: must betrueauthoring.sourceMachine/authoring.targetMachine: optional handoff metadata for the source-machine -> target-machine workflowsetup.configPath: project-local config path to hand to the normal setup pipeline after materializationsetup.outputDir: optional Randal native runner output overridefiles[]: explicit file payloads to materialize before setup runs
Each files[] entry supports:
scope:projectorhomepath: relative path inside that scopekind: artifact label recorded in.randal/managed-artifacts.yamlcontent: 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.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
identity.persona | string | — | No | Agent persona injected into system prompt. Supports prompt resolution. |
identity.systemPrompt | string | — | No | Additional system instructions appended to prompt. Supports prompt resolution. |
identity.knowledge | string[] | [] | No | Glob patterns or file paths for knowledge files. Contents loaded and injected into system prompt. Supports prompt resolution. |
identity.rules | string[] | [] | No | Rules injected as a numbered list into system prompt. Each entry supports prompt resolution. File entries are split by newlines into individual rules. |
identity.vars | Record<string, string> | {} | No | User-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.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
runner.defaultAgent | "native" | "randal-native" | "mock" | "native" | No | Default native runner adapter. |
runner.defaultModel | string | "anthropic/claude-sonnet-4" | No | Default model identifier passed to the native runner (an OpenRouter catalog id — vendor prefixes like openai/… and anthropic/… all route through OpenRouter). |
runner.effort.defaultLevel | eco | pro | ultra | pro | No | Global execution-effort default. It selects a versioned capability route and is independent from the account's billing plan. See Effort routing. |
runner.effort.enabled | boolean | false for legacy configs | No | Enables effort recipe routing. The Settings effort control enables it when saved; omission preserves an upgraded installation's existing defaultModel. |
runner.defaultMaxIterations | number | 20 | No | Maximum iterations per job before marking as failed. |
runner.maxNativeToolTurns | number | 64 | No | Maximum 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.maxDelegationDepth | number | 8 | No | Maximum nested agent delegation depth. Progress/struggle guards remain responsible for stopping non-progressing loops. |
runner.workdir | string | — | Yes | Working directory for agent processes. |
runner.allowedWorkdirs | string[] | — | No | Allowed 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.completionPromise | string | "DONE" | No | Completion marker tag. The runner parses the configured value as <promise>{value}</promise> in agent output; the default is <promise>DONE</promise>. |
runner.struggle.noChangeThreshold | number | 3 | No | Iterations with no file changes before the agent is considered stuck. |
runner.struggle.maxRepeatedErrors | number | 3 | No | Consecutive non-zero exit codes before the agent is considered stuck. |
runner.randalAuth.mode | "disabled" | "openai-oauth" | "disabled" | No | Legacy/debug Randal native runner auth-file materialization before native run. Native OpenAI account auth uses randal provider auth openai instead. |
runner.randalAuth.authFileEnv | string | "RANDAL_RUNTIME_AUTH_JSON" | No | Environment variable name that contains the JSON payload written to the Randal native runner auth file at runtime. |
runner.randalAuth.authFilePath | string | Randal native runner default auth path under ~/.local/share/native/auth.json | No | Optional override for where the runner writes the materialized auth file. Leave unset unless your runtime needs a different location. |
runner.randal.variant | string | — | No | Optional Randal native runner CLI variant flag for explicit legacy/debug Randal native runner runs. |
Loop-State Cutover
.randal/loop-state.jsonis 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, anderroredare 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.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
credentials.envFile | string | "./.env" | No | Path to .env file (relative to config file). |
credentials.allow | string[] | [] | No | Allowlist of variable names to load from the .env file. Only listed vars are passed to the agent. |
credentials.inherit | string[] | ["PATH", "HOME", "SHELL", "TERM"] | No | Environment 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.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
connectors.<id>.source | string | builtin:<id> | No | Connector manifest source. Built-in sources must match the map key, e.g. connectors.notion.source: builtin:notion. |
connectors.<id>.enabled | boolean | false | No | Whether new brain sessions should receive this connector's MCP/tools. |
connectors.<id>.secretScope | user | workspace | secrets.defaultScope | No | Intended secret ownership scope. Local v1 stores through the local user provider. |
connectors.<id>.settings | object | {} | No | Non-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.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
secrets.provider | local-user-env | local-user-env | No | Secret provider implementation. |
secrets.defaultScope | user | workspace | user | No | Default connector secret scope. |
secrets.processEnv.connectors.<connectorId>.<SECRET_NAME> | env var name | — | No | Explicit process env mapping for connector secrets. Values must be uppercase env identifiers and are env var names only, not raw secrets. |
secrets.localUserEnv.workspaceId | string | generated | No | Stable workspace id used in the local env-file path. |
secrets.localUserEnv.workspaceIndexPath | string | ~/.randal/workspaces/index.json | No | Workspace id index. Must stay under ~/.randal unless unsafe paths are explicitly allowed. |
secrets.localUserEnv.envFilePattern | string | ~/.randal/secrets/workspaces/{workspaceId}.env | No | Local user secret file pattern. |
secrets.localUserEnv.projectEnvFallback | boolean | true | No | Allow scoped fallback reads from the project .env. |
secrets.localUserEnv.allowUnscopedProjectEnvFallback | boolean | false | No | Allow legacy unscoped fallback names such as NOTION_API_KEY. Prefer scoped keys. |
secrets.localUserEnv.allowUnsafePaths | boolean | false | No | Allow 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.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
services.<name>.description | string | — | No | Human-readable purpose of this service. |
services.<name>.credentials.type | "env" | "file" | "ambient" | "script" | "none" | — | Yes | Credential delivery mechanism. |
services.<name>.audit | boolean | false | No | Log when agent spawns with this service's credentials. |
Credential Types
type: env — Inject environment variables directly.
| Field | Type | Description |
|---|---|---|
vars | Record<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.
| Field | Type | Description |
|---|---|---|
file | string | Source file path (relative to config). |
mountAs | string | Destination path where the file is copied. |
vars | Record<string, string> | Env vars to inject (typically pointing to the mounted file). |
type: ambient — Keep existing host binaries and config dirs available.
| Field | Type | Description |
|---|---|---|
binaries | string[] | Binary names to ensure stay in PATH. |
paths | string[] | Config directories the agent needs access to. |
type: script — Run a script before each job and capture output as credentials.
| Field | Type | Description |
|---|---|---|
command | string | Script to execute (relative to config). |
vars | Record<string, string> | Map of var names. Use "stdout" as the value to capture script output. |
ttl | number | Re-run interval in seconds. Cached until expired. |
type: none — Explicitly block access to a service.
| Field | Type | Description |
|---|---|---|
binaries | string[] | Binary names to strip from PATH. |
vars | string[] | 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).
| Field | Type | Default | Description |
|---|---|---|---|
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.allow | string[] | [] | PATH prefixes to keep (when mode is allowlist). Supports ~ expansion. |
sandbox.pathFilter.block | string[] | [] | Binary names whose containing dirs are removed (when mode is blocklist). |
sandbox.homeAccess.ssh | boolean | true | Allow ~/.ssh access. When false: sets GIT_SSH_COMMAND=/bin/false, unsets SSH_AUTH_SOCK. |
sandbox.homeAccess.gitconfig | boolean | true | Allow ~/.gitconfig credential helpers. When false: sets GIT_CONFIG_GLOBAL=/dev/null. |
sandbox.homeAccess.docker | boolean | true | Allow ~/.docker/config.json. When false: sets DOCKER_CONFIG=/dev/null. |
sandbox.homeAccess.aws | boolean | true | Allow ~/.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" }.
| Field | Type | Default | Description |
|---|---|---|---|
updates.autoCheck | boolean | false | Check 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.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
gateway.channels | Channel[] | [] | No | Communication channels. At least one HTTP channel is needed for randal serve. |
🌐 HTTP Channel
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
type | "http" | — | Yes | Channel type discriminator. |
port | number | 7600 | No | HTTP server port. |
host | string | 127.0.0.1 | Hosted: Yes | Interface 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. |
auth | string | — | Yes | Bearer token for API authentication. Use ${RANDAL_API_TOKEN} to read from env. |
corsOrigin | string | — | Hosted: Yes | Explicit 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. |
allowQueryTokenAuth | boolean | false | No | Temporary 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.allowedWorkdirsto the mounted workspace directory - keep
allowQueryTokenAuthunset 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
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
type | "discord" | — | Yes | Channel type discriminator. |
token | string | — | Yes | Discord bot token. |
allowFrom | string[] | — | No | Discord user ID allowlist. |
guildId | string | — | No | Guild ID for instant slash command registration. If omitted, uses global registration (~1 hour propagation). |
servers | DiscordServer[] | [] | No | Per-server configuration. See Discord Guide. |
Prefer the Custom Relay Guide for new integrations.
🎙️ Voice Channel
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
type | "voice" | — | Yes | Channel type discriminator. |
allowFrom | string[] | [] | No | Legacy trusted admin caller list. Preserved as an admin-trust input for PSTN callers. |
access.trustedCallers | string[] | [] | No | Additional trusted admin caller E.164 numbers. |
access.unknownInbound | "deny" | "external" | "deny" | No | Unknown inbound caller handling. deny is the secure default. |
access.defaultExternalGrants | string[] | [] | No | Default 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.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
memory.store | "postgres" | "postgres" | No | Storage backend. Postgres is the only supported runtime store. |
memory.index | string | "memory-{name}" | No | Logical memory namespace retained for compatibility with existing config shape. |
memory.autoInject.enabled | boolean | true | No | Auto-inject relevant memory into agent system prompt. |
memory.autoInject.maxResults | number | 5 | No | Maximum memory results to inject per iteration. |
memory.sharing.publishTo | string | — | No | Shared logical scope for cross-agent learnings. Backed by tenant-scoped Postgres rows. |
memory.sharing.readFrom | string[] | [] | No | Shared 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.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
database.profile | "local" | "generic" | "supabase" | "railway" | "local" | No | Runtime profile. Core SQL remains portable; Supabase overlays are optional. |
database.url | string | ${DATABASE_URL} | Hosted: Yes | Postgres URL. Keep secret; diagnostics redact credentials. |
database.schema | string | "public" | No | Schema containing Randal tables. |
database.ssl | "disable" | "require" | profile-dependent | No | Use require for most managed providers. |
database.migrationMode | "manual" | "startup" | "manual" | No | Operators should prefer explicit migrations before deploy. |
database.orgId | UUID | local singleton | No | Tenant org scope for local or cloud identity. |
database.projectId | UUID | local singleton | No | Tenant project/workspace scope. |
database.pool.min | number | 0 | No | Minimum pool size. |
database.pool.max | number | 10 | No | Maximum runtime connections. Tune for provider limits. |
database.pool.idleTimeoutMillis | number | 30000 | No | Idle connection lifetime. |
database.pool.connectionTimeoutMillis | number | 5000 | No | Connection 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 var | Default | Description |
|---|---|---|
RANDAL_RETENTION_DISABLED | unset | Set to 1 to disable the reaper entirely (e.g. audit/compliance holds beyond retention classes). |
RANDAL_RETENTION_SWEEP_INTERVAL_MS | 21600000 (6 h) | Interval between sweeps. |
RANDAL_RETENTION_INITIAL_DELAY_MS | 120000 (2 min) | Delay before the startup sweep — short-lived desktop sidecar sessions still reap without waiting for the first interval tick. |
RANDAL_RETENTION_EPHEMERAL_DAYS | 7 | Max age for rows classed ephemeral. |
RANDAL_RETENTION_STANDARD_DAYS | 90 | Max age for rows classed standard (and unclassed tables like checkpoints/tool_invocations). |
RANDAL_RETENTION_BATCH_SIZE | 1000 | Rows 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:
| Field | Type | Default | Required |
|---|---|---|---|
type | "openai" | — | Yes |
model | string | "text-embedding-3-large" | No |
apiKey | string | — | Yes |
OpenRouter:
| Field | Type | Default | Required |
|---|---|---|---|
type | "openrouter" | — | Yes |
model | string | — | Yes |
apiKey | string | — | Yes |
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:
| Field | Type | Default | Required |
|---|---|---|---|
type | "ollama" | — | Yes |
model | string | — | Yes |
url | string | "http://localhost:11434" | No |
🛠️ tools
Array of external tool definitions. Defaults to [].
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
tools[].name | string | — | Yes | Tool name. |
tools[].binary | string | — | Yes | Executable binary name (must be on PATH). |
tools[].skill | string | — | No | Path to a skill documentation markdown file. |
tools[].platforms | ("darwin" | "linux" | "win32")[] | ["darwin", "linux"] | No | Platforms where this tool is available. |
💰 tracking
Cost tracking configuration. Defaults to {}.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
tracking.tokenPricing | Record<string, { input: number, output: number }> | {} | No | Per-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.
| Field | Type | Default | Description |
|---|---|---|---|
heartbeat.enabled | boolean | false | Enable periodic heartbeat. |
heartbeat.every | string | "30m" | Interval between ticks. Supports: "15m", "1h", "2h30m". |
heartbeat.prompt | string | "./HEARTBEAT.md" | Path to heartbeat prompt file, or inline prompt string. |
heartbeat.activeHours.start | string | — | Start of active window (HH:MM format). |
heartbeat.activeHours.end | string | — | End of active window (HH:MM format). |
heartbeat.activeHours.timezone | string | "UTC" | Timezone for active hours. |
heartbeat.target | string | "none" | Where to send heartbeat results. |
heartbeat.model | string | — | Override 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.
| Field | Type | Default | Description |
|---|---|---|---|
cron.jobs.<name>.schedule | string | {every} | {at} | — | Cron expression, interval, or one-shot time. |
cron.jobs.<name>.prompt | string | — | The prompt to execute. |
cron.jobs.<name>.execution | "main" | "isolated" | "isolated" | main: queue for next heartbeat. isolated: run as standalone job. |
cron.jobs.<name>.model | string | — | Override model for this job. |
cron.jobs.<name>.effort | eco | pro | ultra | — | Pin the scheduled run's effort independently from its owner's later account-default changes. An explicit model still wins route selection. |
cron.jobs.<name>.announce | boolean | false | Whether 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.
| Field | Type | Default | Description |
|---|---|---|---|
hooks.enabled | boolean | false | Enable webhook endpoints. |
hooks.token | string | — | Auth token for webhook requests. If not set, hooks are disabled. |
hooks.path | string | "/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
getVoiceCapabilityreports voice unavailable even when everything below is configured. The scaffolding config stays for when voice returns. Legacyvoice.stt.provider/voice.stt.model/voice.stt.apiKeykeys (andvoice.tts.provider: openai) parse with a startup warning and are ignored.
| Field | Type | Default | Description |
|---|---|---|---|
voice.enabled | boolean | false | Enable voice/video features |
voice.livekit.url | string | "" | LiveKit server URL |
voice.livekit.apiKey | string | "" | LiveKit API key |
voice.livekit.apiSecret | string | "" | LiveKit API secret |
voice.twilio.accountSid | string | "" | Twilio Account SID |
voice.twilio.authToken | string | "" | Twilio Auth Token |
voice.twilio.phoneNumber | string | "" | Twilio phone number |
voice.stt.openrouterModel | string | — | Audio-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.voice | string | — | TTS voice ID |
voice.tts.apiKey | string | "" | TTS provider API key |
voice.turnDetection.mode | "auto" | "manual" | "auto" | Turn detection mode |
voice.video.enabled | boolean | false | Enable video features |
voice.video.visionModel | string | "gpt-4o" | Vision model for processing screen shares |
voice.video.publishScreen | boolean | false | Publish agent screen as video track |
voice.video.recordSessions | boolean | false | Record voice/video sessions |
voice.video.recordPath | string | "./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
| Field | Type | Default | Description |
|---|---|---|---|
mesh.enabled | boolean | false | Enable mesh orchestration |
mesh.role | string (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.expertise | string | {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.endpoint | string | — | This instance's HTTP endpoint for peer communication |
mesh.routingWeights.expertise | number | 0.4 | Weight for semantic expertise matching in routing (2-tier: vector cosine → role match) |
mesh.routingWeights.reliability | number | 0.3 | Weight for reliability score in routing |
mesh.routingWeights.load | number | 0.2 | Weight for current load in routing |
mesh.routingWeights.modelMatch | number | 0.1 | Weight for model availability in routing |
📊 Analytics & Self-Learning
| Field | Type | Default | Description |
|---|---|---|---|
analytics.enabled | boolean | true | Enable analytics engine. Set to false to opt out. |
analytics.autoAnnotationPrompt | boolean | true | Prompt for annotations after job completion |
analytics.feedbackInjection | boolean | true | Inject empirical guidance into system prompts |
analytics.recommendationFrequency | "daily" | "weekly" | "on-demand" | "on-demand" | How often to generate recommendations |
analytics.domainKeywords | Record<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.agingHalfLife | number | 30 | Half-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
| Field | Type | Default | Description |
|---|---|---|---|
browser.enabled | boolean | false | Enable browser automation |
browser.headless | boolean | true | Run browser in headless mode |
browser.profileDir | string | — | Directory for browser profile persistence |
browser.sandbox | boolean | false | Run browser in sandbox container |
browser.viewport.width | number | 1280 | Browser viewport width |
browser.viewport.height | number | 720 | Browser viewport height |
browser.timeout | number | 30000 | Default timeout in milliseconds |
🔎 Built-in Web Search
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.
| Field | Type | Default | Description |
|---|---|---|---|
webSearch.enabled | boolean | true | Expose 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.contact | string | — | Contact URL or email appended to the honest User-Agent sent to API engines (Wikipedia, HN, Marginalia, GDELT, RSS). |
webSearch.marginaliaApiKey | string | — | Marginalia key (free by email); supports ${MARGINALIA_API_KEY} substitution. Omitted → the documented public demo key. |
webSearch.engines.duckduckgo | boolean | true | Primary general-web engine (HTML endpoint, POST). |
webSearch.engines.wikipedia | boolean | true | Entity/reference lane (MediaWiki API). |
webSearch.engines.hackernews | boolean | true | Dev lane (HN Algolia API). |
webSearch.engines.marginalia | boolean | true | Long-tail/indie-web lane. |
webSearch.engines.mojeek | boolean | false | Independent index; its robots.txt disallows /search, so this ships opt-in. |
webSearch.engines.bingNewsRss | boolean | true | Primary news-URL source (RSS). |
webSearch.engines.googleNewsRss | boolean | true | News headline/recency signal only — its article links are not resolvable, so they never appear as results. |
webSearch.engines.gdelt | boolean | true | Global news coverage (GDELT DOC API). |
webSearch.timeoutMs | number | 10000 | Per-engine search request timeout. |
webSearch.extract.timeoutMs | number | 15000 | Per-URL extraction fetch timeout. |
webSearch.extract.maxUrls | number | 10 | Max URLs per web_extract call. |
webSearch.extract.renderJavaScript | boolean | true | Re-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.renderTimeoutMs | number | 20000 | Timeout for a JavaScript-rendered page read. |
webSearch.cache.ttlSeconds | number | 1800 | Search result cache TTL (general topic). |
webSearch.cache.newsTtlSeconds | number | 600 | Search result cache TTL (news topic). |
webSearch.cache.maxEntries | number | 500 | LRU cache size. |
🔄 Runner Extensions
MCP Server
| Field | Type | Default | Description |
|---|---|---|---|
runner.mcpServer.enabled | boolean | false | Enable MCP server for bidirectional agent communication |
runner.mcpServer.port | number | 7601 | MCP server port |
runner.mcpServer.tools | string[] | ["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.