⚙️ 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).
📋 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 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.bootstrapis 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.
| 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 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 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 OpenCode 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 .opencode/loop-state.json state.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
runner.defaultAgent | "opencode" | "mock" | "opencode" | No | Default agent adapter. |
runner.defaultModel | string | "anthropic/claude-sonnet-4" | No | Default model identifier passed to the agent CLI. For Railway GPT Pro / OpenCode OAuth deployments, set this to openai/gpt-5.4. |
runner.defaultMaxIterations | number | 20 | No | Maximum iterations per job before marking as failed. |
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.opencodeAuth.mode | "disabled" | "openai-oauth" | "disabled" | No | Enables OpenCode auth-file materialization before opencode run. Use openai-oauth for the Railway subscription-auth flow. |
runner.opencodeAuth.authFileEnv | string | "OPENCODE_AUTH_JSON" | No | Environment variable name that contains the JSON payload written to the OpenCode auth file at runtime. |
runner.opencodeAuth.authFilePath | string | OpenCode default auth path under ~/.local/share/opencode/auth.json | No | Optional override for where the runner writes the materialized auth file. Leave unset unless your runtime needs a different location. |
runner.opencode.variant | string | — | No | Optional OpenCode CLI variant flag. For GPT Pro Railway usage, set xhigh. |
Loop-State Cutover
.opencode/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.
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:
- Run
opencode auth loginlocally. - Copy
~/.local/share/opencode/auth.jsoninto a secret namedOPENCODE_AUTH_JSON. - Start Railway normally; the runner materializes the auth file before spawning OpenCode.
Security handling:
- Treat
OPENCODE_AUTH_JSONlike 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.
| 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 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.
| 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.tavily.source: builtin:tavily. |
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:
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.
| 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 TAVILY_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 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.
| 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. |
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.
🔌 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 OpenCode 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>.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: 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
| 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.provider | "deepgram" | "whisper" | "assemblyai" | "deepgram" | Speech-to-text provider |
voice.stt.model | string | — | STT model name |
voice.stt.apiKey | string | "" | STT provider API key |
voice.tts.provider | "elevenlabs" | "cartesia" | "openai" | "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 requires LiveKit + STT + TTS configuration. PSTN calling requires Twilio in addition to that browser/media baseline.
🌐 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 |
🔄 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.