Extension patterns

Common shapes for nexo extensions. An extension is a self-contained directory with a manifest.toml that declares contributed tools, advisors, skills, MCP servers, channel adapters, and config schemas. Operators install with nexo ext install ./your-extension.

Pick the closest match; copy the skeleton; modify.


Pattern 1 · Tool bundle

When to use · You have 3-10 related tools (e.g. CRM ops: crm_lookup, crm_create_contact, crm_update_deal, crm_close_deal) and you want to ship them as a unit.

A tool bundle is the simplest extension. Each tool gets its own JSON schema + handler binary (or in-process Rust function). The manifest enumerates them; the daemon registers all on nexo ext install.

[extension]
id = "crm-tools"
version = "0.2.0"
description = "Salesforce-style CRM operations"

[[tools]]
name = "crm_lookup"
schema_path = "tools/crm_lookup.json"
binary = "./bin/crm-tools"

[[tools]]
name = "crm_create_contact"
schema_path = "tools/crm_create_contact.json"
binary = "./bin/crm-tools"

[[tools]]
name = "crm_close_deal"
schema_path = "tools/crm_close_deal.json"
binary = "./bin/crm-tools"

The binary is a single executable that dispatches by tool name. Operators add the tool names to agents.yaml once installed.


Pattern 2 · Advisor pack

When to use · You're shipping domain-specific personas (sales, legal-review, customer-support escalation) that other operators can drop into their agents.

Each advisor is a markdown system-prompt file the agent prepends to its base persona when handling specific topics. Bundle 3-8 together for a vertical.

[extension]
id = "sales-advisor-pack"
version = "0.1.0"
description = "BANT-style qualification + handoff prompts"

[[advisors]]
id = "bant-qualifier"
prompt_path = "advisors/bant_qualifier.md"

[[advisors]]
id = "objection-handler"
prompt_path = "advisors/objection_handler.md"

[[advisors]]
id = "demo-booker"
prompt_path = "advisors/demo_booker.md"

advisors/bant_qualifier.md:

You are a BANT-trained sales qualifier. For every inbound message,
internally score:
- Budget: ...
- Authority: ...
- Need: ...
- Timeline: ...
Only progress to demo-booker advisor when score >= 70.

Pattern 3 · Skill bundle

When to use · You have multi-step workflows (send-quote, escalate-to-human, handoff-to-team) that aren't single LLM turns — they need scripted sequences with branching.

Skills are YAML-defined workflows the agent can invoke. Multi-step with conditionals and tool calls. The extension ships YAML + referenced templates.

[extension]
id = "support-skills"
version = "0.3.1"

[[skills]]
id = "escalate-to-human"
yaml_path = "skills/escalate.yaml"

[[skills]]
id = "schedule-followup"
yaml_path = "skills/followup.yaml"

skills/escalate.yaml:

id: escalate-to-human
description: "Hand off to a human on a Telegram channel"
steps:
  - tool: format_transcript
    args: { last_n: 10 }
  - tool: telegram_post
    args:
      channel: ${ESCALATION_CHANNEL}
      message: |
        ⚠ Escalation request from ${user_id}
        Summary: ${summary}
        Transcript: ${transcript_url}
  - reply: "Te conecto con un agente humano. Te responderá pronto."

Pattern 4 · MCP server bundle

When to use · You're wrapping an external service as an MCP server so multiple agents can use it.

The extension ships a binary that speaks MCP (stdio or HTTP+SSE). Operators register the MCP server via the manifest; agents see its tools as native ones.

[extension]
id = "github-mcp"
version = "1.0.0"

[[mcp_servers]]
id = "github"
command = "./bin/github-mcp"
transport = "stdio"
env_passthrough = ["GITHUB_TOKEN"]

→ See Building an MCP server extension for the full walkthrough.


Pattern 5 · Multi-tenant SaaS extension

When to use · You're building a vertical SaaS (sales / support / marketing) where each tenant gets the same toolkit but isolated state, scoped credentials, per-tenant audit logs.

The extension declares multi_tenant.isolated_state = true. The framework partitions tool state, credentials, and skill output per tenant_id. Agents bound to a tenant only see that tenant's data.

[extension]
id = "sales-saas"
version = "1.2.0"

[[tools]]
name = "crm_lookup"
schema_path = "tools/crm_lookup.json"

[[advisors]]
id = "bant-qualifier"
prompt_path = "advisors/bant.md"

[multi_tenant]
isolated_state = true       # state stored under tenant scope
per_tenant_secrets = true   # secrets resolved per tenant
audit_per_tenant = true     # audit log scoped per tenant

[multi_tenant.quotas]
default = { llm_tokens_month = 1_000_000, agents = 3 }

The microapp layer (above) provisions tenants + assigns this extension to them via admin RPC.

Multi-tenant SaaS guide


Pattern 6 · Channel adapter pack

When to use · You're contributing a new channel kind that's not a subprocess plugin (e.g. a stdlib-friendly one that fits inline as a daemon module).

The extension declares a channel adapter implementation. The framework registers it with the channel registry; agents reference it via channels: [<kind>:<instance>] in agents.yaml.

[extension]
id = "discord-channel"
version = "0.1.0"

[[channel_adapters]]
kind = "discord"
adapter_module = "discord_adapter"   # rust crate path or shared lib
config_schema_path = "discord_config.json"

Most channels ship as plugins (subprocess), not extensions. Use this pattern only when the adapter must run in-process for performance or to share daemon state directly.


Pattern 7 · Config schema extension

When to use · You want to expose a new YAML config block that operators set in agents.yaml or a new file under config/.

The extension declares a JSON Schema for the new config; the daemon merges it into nexo doctor config validation and nexo agent doctor reports.

[extension]
id = "billing-config"
version = "0.1.0"

[[config_schemas]]
section = "billing"
schema_path = "billing.schema.json"
yaml_files = ["billing.yaml"]

Operator's config/billing.yaml:

billing:
  provider: stripe
  webhook_secret: ${STRIPE_WEBHOOK_SECRET}
  default_plan: pro

nexo doctor config will validate the file against your schema.


Pattern 8 · Knowledge-base loader

When to use · You're shipping a curated KB (FAQs, runbooks, playbooks) that should land in the operator's vector store.

The extension ships markdown / JSON documents + a kb_loader hook that imports them into the configured vector store on install.

[extension]
id = "support-kb"
version = "1.0.0"

[[kb_collections]]
id = "support-faqs"
loader = "./bin/load-faqs"     # binary that reads docs/ and emits chunks
docs_dir = "docs/"
embedding_model = "minimax-embed"

The loader runs once at install time + re-runs whenever the operator updates the extension version. Output lands in <state_dir>/<tenant_id>/vector/support-faqs/.


Choosing between patterns

If you...Use
Have related tools to ship togetherTool bundle (1)
Have domain-specific persona promptsAdvisor pack (2)
Have multi-step scripted workflowsSkill bundle (3)
Wrap an external service as MCPMCP server bundle (4)
Build a vertical SaaSMulti-tenant SaaS (5)
Add a new in-process channel kindChannel adapter (6)
Add a new config sectionConfig schema (7)
Ship a curated knowledge baseKB loader (8)

Plugin vs Extension — quick decision

If you find yourself between Plugin and Extension:

  • Choose Plugin when: the work is a separate process, runs in a non-Rust language, or interacts with an external service that has its own connection lifecycle (WebSocket, gateway, push).
  • Choose Extension when: the work is in-process Rust, ships with curated assets (advisors / skills / KBs), or needs tight multi-tenant state isolation.

See also