Skip to main content

Creating a Toby plugin

Toby integrations can ship as installable plugins: standalone executables that Toby discovers, runs as subprocesses, and treats like built-in integrations. A plugin can be written in any language (TypeScript compiled with Bun, Swift, Go, Rust, Python, and so on) as long as it implements the protocol v1 contract below.

Toby remains the source of truth for configuration. Plugins receive credentials and session state on stdin and return JSON on stdout. They must not read or write ~/.toby/ directly.

How it works

For each operation (connect, list tools, run a tool, …), Toby spawns your binary once, passes optional JSON on stdin, reads one JSON object from stdout, and checks the exit code:

~/.toby/plugins/toby-plugin-myapp <command> [subcommand]

Examples:

~/.toby/plugins/toby-plugin-myapp status
~/.toby/plugins/toby-plugin-myapp connect # stdin: config envelope
~/.toby/plugins/toby-plugin-myapp config shape
~/.toby/plugins/toby-plugin-myapp tools list
~/.toby/plugins/toby-plugin-myapp tools execute # stdin: tool request JSON

After the response is parsed, the subprocess exits. Chat tools and connect flows all use this same one-shot pattern.

Language-agnostic

Toby uses ordinary process spawn on the binary path. Your plugin does not need Bun, Node, or any particular runtime on the user's machine—only your compiled executable (or script with a shebang).

Binary naming and discovery

RuleDetail
Nametoby-plugin-<name> where <name> matches ^[a-z0-9_-]+$ (this becomes the CLI integration name)
Location~/.toby/plugins/ (or $TOBY_DIR/plugins/ when TOBY_DIR is set)
PermissionsThe file must be executable (chmod +x)
CollisionsThe name must not match an existing built-in integration

Install with toby plugins install <path>, or copy the binary into the plugins directory manually. Run toby plugins list to confirm discovery.

Streams, exit codes, and limits

stdin, stdout, stderr

StreamRule
stdinUTF-8 JSON when the subcommand requires it; empty or omitted otherwise
stdoutExactly one JSON object, then exit—no log prefixes, banners, or trailing text
stderrHuman-readable diagnostics only; Toby may show stderr on failure but never parses it

When stdin is empty for envelope-based commands, treat config and state as {}.

Exit codes

CodeMeaning
0Success (ok: true in JSON)
1Business failure (ok: false)
2Contract or usage error (bad argv, malformed JSON, unknown subcommand)

Unknown commands or invalid usage should exit 2 with JSON like { "ok": false, "error": "…", "code?": "…" }.

Timeouts

Each subprocess has a 25 second timeout and 4 MiB stdout limit. Long-running API work must finish within that window.

Config envelope (stdin)

Many subcommands read a JSON object from stdin:

{
"config": {
"apiKey": "example"
},
"state": {
"connectedAt": "2026-05-31T12:00:00.000Z"
}
}
FieldMeaning
configCredential and integration fields Toby stores in credentials.json (namespaced as <name>.<key> in the configure UI)
stateSession fields Toby stores in config.json for this integration
validateToolsOptional on status only—when true, return per-tool health rows (see status)

Protocol subcommands

Every plugin should implement the subcommands in this table. Toby invokes them with the argv shown.

argvstdinstdoutPurpose
statusOptional config envelopeStatus responseIdentity, health, chat prep, doctor checks
connectConfig envelope{ ok, reason?, config? }toby connect <name>
disconnectOptional config envelope{ ok, reason?, config? }toby disconnect <name>
config shape(none){ ok, fields? }Configure UI field definitions
config getConfig envelope{ ok, config? }Normalized credential readback
config setConfig envelope{ ok }Optional hook after Toby saves credentials
tools list(none){ ok, tools? }Chat tool catalog
tools executeTool requestTool responseRun one chat tool
setupOptional config envelopeSetup responseOne-time setup (toby plugins setup)
setup guideOptional config envelopeSetup guide responseOnboarding wizard in Toby.app

status

Reports plugin identity, protocol version, connection state, and metadata used by toby status, the configure UI, and toby plugins doctor.

stdin: optional config envelope.

stdout (success):

{
"ok": true,
"name": "myapp",
"displayName": "My App",
"description": "Short description for status and configure",
"version": "1.0.0",
"protocolVersion": "1",
"connected": true,
"capabilities": ["chat"],
"providerCategories": ["tasks"],
"details": "API key configured."
}
FieldRequiredMeaning
okyesMust be true on success
nameyesIntegration CLI name (matches binary suffix)
displayNameyesHuman label in UI and status
descriptionyesOne-line summary
versionyesPlugin release version
protocolVersionyesMust be "1" for this spec
connectedyesWhether Toby should treat the integration as connected
capabilitiesnoDefault ["chat"]. May include "inbound" for daemon @mention listening
providerCategoriesnoe.g. email, calendar, tasks, contacts, chat, search, work_tracker
detailsnoExtra status text
resourcesnoArbitrary tags for status output
setupAvailablenotrue when the plugin implements setup
setupDescriptionnoShort label for setup in configure / install prompts

Optional extensions on status:

  • authMethods — OAuth or multi-method auth, same shape as built-in integrations:

    "authMethods": [
    { "id": "oauth_pkce", "label": "OAuth (PKCE)", "isDefault": true },
    { "id": "api_key", "label": "API Key" }
    ]
  • chatModelPrepRequired when capabilities includes "chat". Supplies integration-specific prompt sections Toby merges with personas and global tool guidance:

    "chatModelPrep": {
    "systemPromptSection": "### My App\nShort block for multi-integration chat.",
    "singleSessionRules": "You are Toby…\nRules:\n- …",
    "singleSessionUserTemplate": "User request:\n{{userPrompt}}",
    "multiUserContentTemplate": "## My App\n…\n{{userPrompt}}"
    }

    Use {{userPrompt}} in templates where the user's message should appear.

  • chatReadiness — When stdin includes a config envelope, tell the chat UI whether the integration is ready:

    "chatReadiness": {
    "ok": false,
    "hint": "Run `toby connect myapp` after configuring credentials."
    }
  • tools — When stdin has "validateTools": true, return per-tool health rows:

    "tools": [
    { "tool": "myTool", "ok": true, "details": "Reachable." }
    ]

connect

Validates configuration and confirms the integration can be used. Invoked by toby connect <name>.

stdin: config envelope (required config).

stdout:

{ "ok": true, "reason": "Connected successfully." }

or

{ "ok": false, "reason": "API key is required." }

When ok is true, Toby writes connectedAt into integration state. You may return a config object to persist tokens or normalized fields (OAuth access/refresh tokens, etc.); Toby merges it into stored credentials.


disconnect

Acknowledges disconnect. Toby clears session state regardless; use this to release remote resources or wipe sensitive credential fields via a config writeback.

stdin: optional config envelope.

stdout:

{ "ok": true, "reason": "Disconnected." }

config shape

Returns field definitions for Configure → Integrations. Toby namespaces keys as <name>.<key>.

stdin: none.

stdout:

{
"ok": true,
"fields": [
{
"key": "apiKey",
"label": "API Key",
"type": "string",
"required": true,
"masked": true
}
]
}
Field propertyMeaning
keyLocal field id (Toby prefixes with integration name)
labelUI label
typestring, number, boolean, or select
required, masked, multilineOptional UI behavior
optionsRequired for select type
default, pattern, minLength, maxLength, descriptionOptional validation and help text
showForAuthMethodsOptional list of auth method ids—show field only for those methods

config get

Optional normalization hook. Toby already stores values from the configure UI; this subcommand lets the plugin return cleaned or inferred config.

stdin: config envelope.

stdout:

{
"ok": true,
"config": { "apiKey": "example", "authMethod": "api_key" }
}

config set

Optional sync hook after Toby saves credentials (remote registration, hydration, etc.).

stdin: config envelope.

stdout:

{ "ok": true }

tools list

Returns the chat tool catalog for this integration.

stdin: none.

stdout:

{
"ok": true,
"tools": [
{
"name": "myappEcho",
"description": "Echo a message back",
"readOnly": true,
"inputSchema": {
"type": "object",
"properties": {
"message": { "type": "string", "description": "Text to echo" }
},
"required": ["message"]
}
}
]
}

Each tool requires name, description, and inputSchema (JSON Schema object root with properties, required, and primitive types). Optional: readOnly (default false). Mark read-only tools when they do not mutate remote state; Toby may cache their results within a chat turn.


tools execute

Runs one tool during chat.

stdin:

{
"tool": "myappEcho",
"input": { "message": "hello" },
"config": { "apiKey": "example" },
"state": {},
"dryRun": false
}
FieldMeaning
toolTool name from tools list
inputArguments matching the tool's inputSchema
config / stateCurrent integration config and session state
dryRunWhen true, mutating tools should preview changes without applying them

stdout (success):

{
"ok": true,
"result": { "echo": "hello" },
"appliedActions": ["Echoed message"]
}
FieldMeaning
resultJSON value returned to the model (structure is up to your plugin)
appliedActionsHuman-readable lines describing side effects (shown in the chat transcript)
configOptional writeback (token refresh, updated remote ids, etc.)

stdout (failure):

{
"ok": false,
"error": "Unknown tool: missing"
}

Honor dryRun for mutating tools. Return appliedActions whenever the tool would change something in production mode.


setup (optional)

For one-time setup (installing macOS Shortcuts, downloading models, etc.). Advertise on status with setupAvailable: true and optional setupDescription.

stdin: optional config envelope.

stdout:

{
"ok": true,
"actions": [
{
"id": "step-one",
"label": "Install helper shortcut",
"ok": true,
"skipped": true,
"detail": "Already installed."
},
{
"id": "step-two",
"label": "Open import dialog",
"ok": true,
"detail": "Complete the import in Shortcuts.app."
}
]
}
Action fieldRequiredMeaning
idyesStable machine id
labelyesHuman-readable step name
okyesStep succeeded (including already satisfied when skipped: true)
skippednoStep not run because prerequisites are already met
detailnoExtra explanation for the user

Top-level ok: true means the setup command ran. Use top-level ok: false only for fatal errors. Individual step failures can set ok: false on that action while top-level ok stays true.

Setup is idempotent—plugins detect whether work is already done; Toby does not persist setup completion separately.


setup guide (optional)

Provide a guided onboarding experience for the native Toby.app. When a user opens an integration in the app's configure view and taps Setup Guide, Toby runs toby-plugin-<name> setup guide and renders the returned steps.

stdin: optional config envelope.

stdout:

{
"ok": true,
"name": "myapp",
"displayName": "My App",
"description": "Short description for the wizard header",
"steps": [
{
"id": "overview",
"title": "What My App can do",
"description": "One or two sentences about the integration."
},
{
"id": "provider",
"title": "Create credentials in the provider console",
"links": [
{ "label": "Open provider console", "url": "https://example.com/apps" }
],
"artifacts": [
{
"id": "redirectUri",
"label": "Redirect URI",
"value": "http://localhost:9876/callback",
"hint": "Paste this into the provider's OAuth redirect settings."
}
]
},
{
"id": "credentials",
"title": "Add credentials",
"description": "Paste the API key or client secret into the fields below."
},
{
"id": "validate",
"title": "Validate",
"description": "Toby will run a health check to confirm the integration is ready."
}
]
}
Step fieldRequiredMeaning
idyesStable machine id
titleyesHeading shown in the wizard
descriptionnoLonger explanation
linksnoArray of { label, url } buttons
artifactsnoArray of { id, label, value, hint? } copyable values

If your plugin does not implement setup guide, Toby builds a generic guide from status, config shape, and authMethods. Custom guides are especially useful for OAuth integrations so users know exactly which redirect URI and scopes to use.


Inbound chat (optional, advanced)

Plugins that listen for @mentions or DMs in a chat platform declare "inbound" in capabilities (in addition to "chat" for tools). Inbound uses a different transport: a long-lived subprocess and newline-delimited JSON (NDJSON), not the one-shot contract above.

toby-plugin-<name> inbound run
StreamRule
stdinOne JSON object per line (messages from Toby)
stdoutOne JSON object per line (messages to Toby)
stderrDiagnostics only

Toby spawns inbound run when the daemon starts inbound for that integration. The process stays alive until Toby sends { "type": "shutdown" } or the daemon stops.

Optional status.inboundPrep metadata:

"inboundPrep": {
"externalKeyFormat": "slack:{teamId}:{channelId}:{threadRootTs}",
"transportLabel": "socket_mode"
}

Plugin → Toby (stdout lines):

typeMeaning
readyTransport connected
eventNormalized inbound user message
personaAppendixResponse to a persona appendix request
errorFatal transport error

Toby → plugin (stdin lines):

typeMeaning
startInitial config, state, dryRun—connect transport, then emit ready
configCredential patch while running
deliverReplyPost assistant reply to a conversation
deliverAskUserPost an askUser prompt
statusUpdate / statusClearTransient status UI in the chat platform
getPersonaAppendixRequest persona-specific appendix text for a turn
shutdownStop and exit

See the Slack plugin in the Toby repository for a full inbound reference.

Managing plugins (Toby commands)

CommandPurpose
toby plugins listList discovered binaries under ~/.toby/plugins/
toby plugins install <path>Validate and copy (or symlink) a plugin into the plugins directory
toby plugins doctorValidate naming, status, protocol version, and tools list for every discovered plugin
toby plugins inspect <name>Show metadata and tool catalog for one plugin
toby plugins setup <name>Run optional one-time setup
toby plugins uninstall <name>Remove the binary and purge stored credentials, config, defaults, and cached tool results

install flags:

FlagEffect
--forceOverwrite an existing managed install
--linkSymlink instead of copy (useful while developing locally)
--setupRun setup after install without prompting (requires a TTY when setup needs interaction)
--no-setupSkip the post-install setup prompt

install validates the same checks as doctor: binary name, successful status, supported protocolVersion, and parseable tools list. It rejects names that collide with built-in integrations.

After install:

toby plugins doctor
toby config # configure credentials
toby connect myapp
toby status integration -i myapp
toby chat --integration myapp "try my tools"

Authoring checklist

  1. Name the binary toby-plugin-<name> and make it executable.
  2. Implement all core subcommands with single-object JSON on stdout and stable exit codes.
  3. Accept config via stdin; never read ~/.toby/ from the plugin process.
  4. Declare at least one chat tool in tools list when capabilities includes "chat".
  5. Return chatModelPrep on status for chat-capable plugins.
  6. Honor dryRun in tools execute for mutating tools.
  7. Return appliedActions strings when tools change remote state.
  8. Install with toby plugins install, then run toby plugins doctor.
  9. Optional: implement setup and set setupAvailable on status.
  10. Optional: implement setup guide for a native-app onboarding wizard.
  11. Optional: implement inbound run when the integration should respond to daemon @mentions.

Reference implementations

The Toby repository includes working plugins you can copy from:

PluginLanguageNotes
toby-plugin-sampleTypeScript (Bun --compile)Minimal protocol surface—start here
toby-plugin-gmailTypeScriptOAuth, auth methods, token writeback
toby-plugin-todoistTypeScriptAPI key auth, task tools
toby-plugin-azureadTypeScriptFull parity migration example
toby-plugin-jiraSwiftmacOS-only, no embedded JS runtime
toby-plugin-websearchSwiftAPI-key search; global webSearch bridge in Toby core
toby-plugin-applecalendarSwiftEventKit + Calendar.app
toby-plugin-macosSwiftSystem controls; optional setup for Shortcuts
toby-plugin-slackTypeScriptChat tools + inbound run (Socket Mode)

Build examples from a git clone:

bun run build:plugin:sample
toby plugins install ./dist/toby-plugin-sample --link --force
toby plugins doctor

Release archives bundle first-party plugins into ~/.toby/plugins/ automatically; developers link local builds as shown above.

Next steps