MINARA

Custom Agents

Saved bundles of system prompt + skills + tools + risk ceiling + triggers, runnable from CLI, REPL, HTTP, workflows, and cron

🟢 Configurable — a Custom Agent is a recipe, not a separate runtime. It runs on the same agent loop, the same skill registry, and the same fund-confirm gate as the REPL session.

A Custom Agent is a saved bundle of:

  • a system_prompt (instructions the agent runs with)
  • a skill_ids subset (which DomainSkills the runner activates)
  • a tool_names + tool_sets subset (which tools the runner exposes)
  • a risk_tier_max ceiling (1 read-only → 4 manual-only). The Web UI wizard presents this as three choices — Read-only, No fund moves, and Can move funds — where the last offers tier 3 (confirmed spot trades: swap, buy, sell) or tier 4 (all funds, incl. perps, withdraw, autopilot). Scheduled and event agents can't move funds unless they opt in to autonomous fund moves (capped at tier 3, confirmed spot trades); each fund move still requires confirmation.
  • a discovery_mode (how aggressively the runner can self-expand)
  • a triggers list (manual, cron, event)
  • free-form metadata

The bundle persists in SQLite (agent_definitions). It is runnable from REPL /agents run, CLI minara agents run, HTTP POST /v1/agents/:id/runs, the Web UI Run drawer, a workflow step (agent_turn { agent_id }), or a cron / event trigger.

Custom Agents are closer to Claude Code subagents than to Anthropic's Managed Agents API: they execute locally inside the same process as the main agent, share the same skill catalog, and inherit the same safety envelopes. They are not a sandboxed third-party runtime.

Custom Agent flow: a saved definition fires on a trigger, the runtime clamps its risk ceiling, it runs on the shared agent loop behind the fund-confirm gate, and notifies you when an autonomous run finishes

Why operators want them

  • Reuse: encode a recurring task once. "Daily ETH morning brief", "Weekly portfolio review", "On-chain inflow watcher" — each lives as a named agent you launch with one click.
  • Scoped tool surface: a researcher agent only needs read-only skills (no swap_tokens). A treasury agent only needs Hyperliquid perps. Smaller skill sets mean tighter system prompts, fewer irrelevant tool calls, and lower token cost per turn.
  • Triggers: an agent attached to cron: "0 9 * * *" runs every morning without operator intervention. An agent attached to event: { event_name: "price.alert" } fires when the event bus publishes a match.
  • Workflows compose them: a multi-step workflow can reference agent_turn { agent_id: "research-bot" } instead of inlining the research prompt — the workflow author owns the flow, the agent owns the reasoning.
  • Shareable: drop a JSON file in ~/.minara/agents/ and the loader picks it up on the next boot. Export your research-bot to a teammate; their instance loads it as source: "file:...".

Three discovery modes

The discovery_mode controls how the runner treats skill_ids and tool_names at execution time.

New agents default to free_discovery: like the main assistant, the agent discovers the skills it needs, so the Web UI wizard doesn't ask you to pick any up front. Tools follow from the selected skills plus the risk_tier_max ceiling, so tool_names, tool_sets, and discovery_skill_pool are advanced fields you set via the JSON file loader or the HTTP API, not the wizard.

constrained

Hard whitelist. The runner activates exactly the declared skill_ids and exposes exactly the declared tool_names (intersected with the union of tool_sets). The activate_skills meta-tool is disabled, so the LLM cannot self-expand mid-turn.

Pick this for production reliability. The agent runs the same shape every time. Easy to review, easy to test, easy to budget.

scenario_aware

Retained for back-compat; behaves identically to constrained today. The scenario classifier that used to preload skills per prompt was retired, so this mode runs the exact declared skill_ids with activate_skills disabled. Existing agents keep the value; new agents should pick constrained.

free_discovery (default)

Start from the declared seed set, plus the LLM may call activate_skills to add skills mid-turn — bounded by discovery_skill_pool glob patterns (empty/missing = the whole catalog) and clamped by risk_tier_max.

Use sparingly: it gives the agent the full skill catalog as a solution space, which is powerful for open-ended research and brittle for narrow recurring jobs.

Safety invariants across all three modes

  • risk_tier_max is enforced at trigger time by the runner, not just at upsert time. Even if you UPDATE agent_definitions SET risk_tier_max = 4 directly in the DB, a cron / event / autopilot trigger still clamps the effective ceiling to 2.
  • The fund-confirm helper (shouldExecuteFundMovingCall) sees the agent run's context via an AsyncLocalStorage. A test run forces the two-step confirm even when MINARA_SKIP_FUND_CONFIRM=1 is set in the environment.
  • Archived agents fail-closed. Any in-flight agent_turn step that references an archived agent continues against its stored snapshot, but new runs against the archived id fail immediately.

Triggers

Each agent has a triggers list. v1 ships four kinds.

KindShapeFires when
manual{ kind: "manual" }You click Run in the UI / type /agents run / POST /v1/agents/:id/runs
cron{ kind: "cron", expr: "0 9 * * *", timezone?: "UTC" }The TriggerManager tick crosses the cron expression
event{ kind: "event", filter: { event_name: "price.alert", payload_match?: {...} } }The event bus publishes a matching event
once{ kind: "once", fire_at: 1717430400000 }The wall-clock time reaches fire_at (unix ms). Fires a single time, then the agent archives itself

A once agent is how a reminder is modeled. It fires one time, then archives so it never fires again. Restoring a fired reminder does not replay it. Reschedule it (set a new fire_at) or run it manually. For a repeating reminder, use a cron trigger instead.

Safety rule: by default any non-manual trigger forces risk_tier_max ≤ 2 at upsert time, so a scheduled / event agent can't move funds. An agent can opt in to autonomous fund moves (allow_autonomous_fund_moves: true, which requires confirm_autonomous_fund_moves: true on the same write); that raises the non-manual ceiling to ≤ 3 (confirmed spot trades). Tier 4 (perps, withdrawals, autopilot) always requires a manual trigger, so a human is in the loop when they fire. The store rejects the write with a clear error if you exceed the ceiling.

Notifications

When a run finishes, Minara can tell you two ways.

In-app notification center. The bell in the top-right shows an unread count and a list of recent agent runs. Each entry links to the run that produced it. It is always on, updates live, and keeps a short history you can mark read or clear.

Browser notifications. Turn on "Browser notifications" in Settings → General, or from the bell's quick toggle, to get a system notification on your desktop when the Minara tab is in the background or minimized. When you are looking at the Minara tab, the alert shows as an in-app message instead. The browser asks for permission the first time. Nothing arrives once you close the tab or quit the browser, and the page must be secure (HTTPS), with localhost as the exception.

Which runs notify. A notification fires for runs an agent makes on its own: scheduled runs (cron), event triggers, one-off reminders, and workflow steps. A run you start yourself does not, since you are already watching it: the Run button, /agents run, or a chat turn. Test runs never notify.

Per-agent control. Each agent has an "In-app notifications" setting. Turn it off for a noisy agent, or choose to be told only when a run fails. New agents have it on.

Workflows that reference agents

A workflow agent_turn step has two forms:

// Preferred: reference an existing Custom Agent by id
{ "kind": "agent_turn", "agent_id": "research-bot",
  "input": { "ticker": "ETH" } }

// Legacy fallback: inline goal — the engine spins up a one-shot
// agent with no scoped skill / tool subset
{ "kind": "agent_turn", "goal": "summarize recent ETH news" }

When to use agent_turn { agent_id } vs tool_call

Use agent_turn when the step needs reasoning, multi-tool exploration, or natural-language output: research, drafting, classification, summarization.

Use tool_call when the step is a single deterministic action on a known input: a swap, a transfer, a balance check, a price fetch. Tool calls are faster, cheaper, and stay on the existing fund-confirm gate without re-litigating the confirmation context across an LLM turn.

A workflow that "shorts ETH then writes a strategy note" splits into two steps: a tool_call { tool: "open_perps_position" } for the execution, then an agent_turn { agent_id: "strategy-note" } for the writeup.

The local-workflow authoring skill enforces this matrix automatically — it surfaces the catalog of Custom Agents and routes "drafting" / "research" / "summarize" intents through agent_turn, and routes fund-moving intents through tool_call.

CLI

The minara agents subcommand exposes the full lifecycle.

ActionShapePurpose
listminara agents list [--archived]Print every registered agent
getminara agents get <id>Dump the full definition as JSON
createminara agents create --from <file.json>Insert from a JSON file
updateminara agents update <id> --from <patch.json>PATCH; bumps version on meaningful change
archiveminara agents archive <id>Soft-delete; tears down attached triggers
runminara agents run <id> [--input "..."] [--test]Launch an ad-hoc run; streams events to stdout
exportminara agents export <id>Print the JSON for piping into a file
importminara agents import [--from <file.json>]Inverse of export; reads stdin when --from is omitted

run opens a synchronous event stream until the workflow completes or fails, then prints the final __adhoc__ output payload. Pipe- friendly for cron / shell scripts.

minara agents export research-bot > research-bot.json
scp research-bot.json bob@host:~/.minara/agents/
ssh bob@host minara agents list           # research-bot appears with source=file:...

REPL

The /agents slash command is the interactive counterpart.

FormWhat it does
/agents or /agents listList every registered agent
/agents createInteractive collectArgs flow: name → system prompt → skill picker
/agents run [<id>] [<input>...]Run an agent; live picker if <id> omitted
/agents archive [<id>]Archive; live picker if <id> omitted; y/N confirm

The /agents create flow uses the standard collectArgs pattern — required fields prompt one at a time, validators re-ask on bad input, three empty answers cancel.

Web UI

The Web UI lists Custom Agents at /agents under the AUTOMATE nav group (alongside Workflows and Autopilot). See /agents in the web UI.

  • List page — every registered agent as a card with name, risk ceiling, discovery mode, trigger summary, and per-card Run / Edit / Archive actions. An Import button accepts a JSON file upload; a New button opens the wizard.
  • Wizard (3 steps) — Step 1 picks a starter template (researcher / treasury sentinel / drafting) or "blank". Step 2 names the agent, writes the system prompt, and picks skills (only the hard-whitelist mode shows the picker). Step 3 chooses the discovery mode, risk ceiling, and trigger kind, with the safety clamp shown inline.
  • Detail page — Overview / Definition / History tabs. The Definition tab shows the live JSON; PATCH edits bump version atomically.
  • Run drawer — opens from any list card or the detail page. Inline SSE event stream shows workflow:startedworkflow:step events → terminal workflow:completed / workflow:failed. Run as test (?test=1) is a checkbox; test runs surface the fund-confirm modal even when the env var skip is set.

REST endpoints

The HTTP gateway exposes the same surface for non-CLI integrations.

MethodPathPurpose
GET/v1/agentsList definitions (?archived=true returns archived only)
POST/v1/agentsCreate — body is an AgentDefinitionInput
GET/v1/agents/:idGet the latest definition
PATCH/v1/agents/:idUpdate; body is a partial AgentUpdatePatch
DELETE/v1/agents/:idArchive (soft-delete; reversible)
POST/v1/agents/:id/restoreRestore an archived agent (re-arms triggers)
DELETE/v1/agents/:id/permanentDelete permanently (irreversible; run history survives)
POST/v1/agents/:id/runsStart an ad-hoc run; append ?test=1 for test mode
GET/v1/agents/:id/runsList past runs (workflow instance history)
GET/v1/workflows/:id/instances/:iid/events/streamSSE event stream for a running instance

The SSE endpoint is the same one used by the Web UI Run drawer. It transparently filters events to the single instance_id.

Example: create via curl

curl -X POST http://localhost:8080/v1/agents \
  -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d @research-bot.json

# Kick off an ad-hoc test run
curl -X POST "http://localhost:8080/v1/agents/research-bot/runs?test=1" \
  -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "input": "summarize recent ETH news" }'

AgentDefinition JSON example

{
  "id": "research-bot",
  "name": "Daily Research Bot",
  "description": "Morning brief: trending tokens, macro context, watchlist signals.",
  "system_prompt": "You are a markets research assistant. Output a 5-bullet brief covering crypto majors, US equities open, and any flagged watchlist alerts. No advice; observation only.",
  "skill_ids": [
    "analysis.market_overview",
    "research.knowledge_base",
    "minara.core"
  ],
  "tool_names": ["get_price", "get_trending", "search_tokens"],
  "tool_sets": ["market_data"],
  "risk_tier_max": 1,
  "discovery_mode": "constrained",
  "triggers": [
    { "kind": "cron", "expr": "0 9 * * *", "timezone": "UTC" }
  ],
  "metadata": {
    "tags": ["research", "daily"],
    "owner": "[email protected]"
  }
}

Notes:

  • risk_tier_max: 1 (read-only) suits a cron-triggered agent that only reads market data. A scheduled agent is capped at tier 2 by default, or tier 3 if it sets allow_autonomous_fund_moves. Raising it past that ceiling is rejected at upsert.
  • discovery_mode: "constrained" means the runner will not call activate_skills mid-turn. The skill / tool surface is exactly what's declared.
  • tool_sets: ["market_data"] layers in every tool in that set alongside the tool_names whitelist. The runner takes the union.

Workflow JSON with agent_turn { agent_id }

{
  "id": "morning-brief",
  "name": "Morning Brief",
  "version": 1,
  "steps": [
    {
      "id": "step_1",
      "kind": "tool_call",
      "tool": "get_portfolio_snapshot",
      "input": {}
    },
    {
      "id": "step_2",
      "kind": "agent_turn",
      "agent_id": "research-bot",
      "input": {
        "portfolio_summary": "{{ steps.step_1.output }}"
      }
    },
    {
      "id": "step_3",
      "kind": "tool_call",
      "tool": "send_telegram",
      "input": {
        "text": "{{ steps.step_2.output }}"
      }
    }
  ]
}

The workflow author wires the flow; research-bot owns the reasoning. When step 2 first executes, the engine snapshots the agent definition into workflow_instances.agent_def_snapshot — later PATCHes to research-bot do not affect this in-flight instance. New runs pick up the new version.

JSON file sharing

The agent loader scans ~/.minara/agents/ (or $MINARA_DATA_DIR/agents/) on boot. Every *.json file that parses as an AgentDefinition lands in the store with source: "file:<absolute_path>".

File-sourced rows are PATCH-locked: REST and CLI updates against them are rejected with a clear error. Edit the file on disk, then restart the agent (or call the loader sync endpoint) to pick up the change. This guarantees the file stays the source of truth — no silent drift between disk and DB.

Removing the file archives the row on the next sync, so file-deletes do not break workflows that already snapshotted the agent.

Safety posture (summary)

  • Runtime risk clamp — non-manual triggers (cron, event, autopilot) clamp effective_risk_tier_max to 2, or to 3 when the agent sets allow_autonomous_fund_moves. The clamp runs in the AgentRunner immediately before the LLM turn, after the trigger source is resolved. Tier-4 tools stay blocked for autonomous sources at the registry gate.
  • Context-aware fund-confirm — every fund-moving tool handler routes through shouldExecuteFundMovingCall(args, ctx). When ctx.test_run === true, the helper forces the two-step confirm regardless of MINARA_SKIP_FUND_CONFIRM. Test runs never silently move money.
  • Archived agents fail-closed — a manual run against an archived agent fails immediately. A cron trigger on an archived agent skips three times in a row, then the trigger is automatically deactivated. In-flight agent_turn steps continue against their stored snapshot — archiving is a future-only operation.
  • Snapshot isolation — workflow instances that reference an agent by agent_id snapshot the full definition at first execution. Later PATCH or ARCHIVE on the source agent does not affect any in-flight instance.
  • Audit log — every run writes the requested risk tier, the effective (clamped) risk tier, the trigger source, and the lists of dropped skills / tools to the audit log. The Web UI surfaces these on the History tab; the CLI surfaces them via minara agents get <id> followed by an instance query.

See Fund-Moving Confirm for the handler contract and Audit & Overrides for the full audit surface.

On this page