Build a Groovy Integration
Step-by-step guide to ship an integration in under 10 minutes. Use the dashboard UI for manual setup, or the API for scripted setup. You can also copy the full AI build prompt below and let your agent generate the manifest for you.
Path A: Dashboard UI
Best if you are setting up one integration manually.
1. Open Dashboard and click Integrations.
2. Click Create Integration.
3. Fill Slug, Name, choose a runtime target, and paste your manifest JSON.
4. Click Create & Activate.
5. Open the integration detail, click Install Integration, then paste Config (JSON) and Secrets (JSON) and click Save Connection.
6. If you use cli_action, open Runners and click Register Runner.
Path B: API / curl
Best if you want reproducible setup, automation, CI, or generated onboarding scripts.
1. Generate your manifest JSON.
2. Call POST /api/extensions.
3. Call POST /api/extensions/{id}/install.
4. Call POST /api/extensions/{id}/connection.
5. If you need a Customer Runner, register it with POST /api/extensions/runners.
6. If you need to bind a specific runner to an install, use the API install call with runnerId. The dashboard UI does not expose runner assignment yet.
Full AI build prompt
Paste into Claude, ChatGPT, or any AI agent. Open the prompt below only if you want to inspect or copy the full instructions.
After your AI generates the manifest, either paste it into the dashboard Integrations flow or use the API steps below.
Understand the manifest
A Groovy integration is an Extension Pack — a single JSON object that declares your tools, how to execute them, and how to describe them to the AI.
{
"schemaVersion": 1,
"displayName": "Your Product Name",
"description": "One sentence about what your product does",
"capabilityTags": ["ops", "incidents"],
"skillInstructions": "Use this integration when the user asks about incidents or on-call.",
"tools": [
{ /* ...tool definitions... */ }
]
}displayNameShown in the Groovy dashboard and chat source chipsdescriptionShort description of your product, shown to admins in the integrations panelcapabilityTagsHelp Groovy find your tools when the user asks relevant questions — acts as a pre-routing indexskillInstructionsNatural language guidance injected into the AI prompt — teach Groovy when to prefer your tools over othersDefine your tools
Each tool represents one action the AI can take. The AI reads the description to decide when to use it, and validates user input against the inputSchema before execution.
Groovy calls your API server-side. Use {{connection.field}} for auth and {{arg_name}} for user-provided values.
{
"slug": "list_incidents",
"name": "List Incidents",
"description": "List open incidents, optionally filtered by severity",
"riskLevel": "read",
"authScope": "end_user",
"inputSchema": {
"type": "object",
"properties": {
"severity": {
"type": "string",
"enum": ["low", "medium", "high", "critical"],
"description": "Filter by severity level"
},
"limit": {
"type": "integer",
"description": "Max results to return",
"minimum": 1,
"maximum": 100
}
}
},
"action": {
"kind": "http_action",
"method": "GET",
"url": "{{connection.base_url}}/api/v2/incidents",
"headers": {
"Authorization": "Bearer {{connection.api_token}}"
},
"query": {
"severity": "{{severity}}",
"limit": "{{limit}}"
},
"timeoutMs": 10000
}
}Risk levels cheat sheet
readSafe, no side effectswriteCreates or modifies datadestructiveDeletes or is irreversibleprivilegedAdmin / elevated accessOptional tool fields
outputSchemaJSON Schema for the expected response. Not enforced at runtime, but documents your API contract.promptHintExtra guidance injected per-tool into the AI prompt. Use for nuance like "prefer this over list_all when the user specifies a filter".tagsCategorization tags for the tool (e.g. ["read-only", "billing"]). Used for future capability routing.enabledSet to false to include the tool in the manifest but hide it from Groovy at runtime. Defaults to true.runtimeTargetOverride the extension default per-tool. One tool can be groovy_cloud while another is customer_runner.maxResponseChars(HTTP actions) Truncate response bodies larger than this. Default: 12000 chars.maxOutputChars(CLI actions) Truncate CLI stdout larger than this. Sent to runner in the request.env(CLI actions) Environment variables passed to the runner. Supports templates like {{connection.api_key}}.message(Connector actions) Custom progress message shown in chat while the action runs.Template syntax
Use {{...}} in URLs, headers, query params, request bodies, and CLI args. Values are resolved at execution time.
{{arg_name}} → Tool argument from inputSchema
{{connection.base_url}} → Saved connection config field
{{connection.api_token}} → Saved connection secret (decrypted at runtime)
{{runtime.user_id}} → Current Groovy user ID
{{runtime.trace_id}} → Current execution trace ID
{{runtime.session_id}} → Current orchestrator session
{{runtime.turn_id}} → Current turn (for billing)
{{runtime.device_id}} → Paired connector device ID
If the entire value is a single {{...}}, the resolved
type is preserved (object/array stays an object/array).
Inside a larger string, it's stringified.Register with Groovy
You have two valid options here. Use the dashboard if you are doing this manually, or use the API if you want automation or CI.
Open Dashboard and click Integrations in the header.
Create the integration, then open its detail page to install it and save the connection.
If the integration uses cli_action, register a runner from the Runners view.
If you need to bind a specific runner to the installation, use the API install call with runnerId for now.
curl -X POST https://your-groovy-url/api/extensions \
-H "Content-Type: application/json" \
-b "your-session-cookie" \
-d '{
"agentId": "YOUR_AGENT_ID",
"slug": "acmeops",
"name": "AcmeOps",
"description": "Incident management for AcmeOps",
"runtimeTargetDefault": "groovy_cloud",
"manifest": { ... your manifest JSON ... },
"activate": true
}'
# Response includes: { extension: { id: "EXT_ID" }, version: { id: "..." } }curl -X POST https://your-groovy-url/api/extensions/EXT_ID/install \
-H "Content-Type: application/json" \
-b "your-session-cookie" \
-d '{
"approvalPolicy": {
"requireApprovalForRiskLevels": ["destructive", "privileged"]
}
}'curl -X POST https://your-groovy-url/api/extensions/EXT_ID/connection \
-H "Content-Type: application/json" \
-b "your-session-cookie" \
-d '{
"config": {
"base_url": "https://api.acme.com"
},
"secrets": {
"api_token": "sk-acme-your-token-here"
}
}'
# Secrets are encrypted with AES-256-GCM before storage.
# They are never returned to the client.Test it
Go to the Groovy dashboard and ask something that should trigger your integration. Groovy will show a source chip like Using AcmeOps when it uses your tool.
"List all high severity incidents"
Found 3 high severity incidents: INC-4821 (Checkout failures), INC-4819 (API latency spike), INC-4815 (Database connection pool exhausted).
Optional: Set up a Customer Runner
If your integration uses cli_action, you need a runner in your environment. The runner is a small HTTPS server that receives typed execution requests from Groovy.
curl -X POST https://your-groovy-url/api/extensions/runners \
-H "Content-Type: application/json" \
-b "your-session-cookie" \
-d '{
"agentId": "YOUR_AGENT_ID",
"name": "production-east",
"endpoint": "https://runner.internal.acme.com:8443",
"authToken": "runner-secret-token-here",
"status": "online"
}'
# Response: { runner: { id: "RUNNER_ID" } }curl -X POST https://your-groovy-url/api/extensions/EXT_ID/install \
-H "Content-Type: application/json" \
-b "your-session-cookie" \
-d '{
"runtimeTargetOverride": "customer_runner",
"runnerId": "RUNNER_ID"
}'Error handling
Groovy returns structured errors to the AI so it can explain failures to the user clearly.
Template resolution error with the missing field name{ needsConnection: true, extension: "slug", authScope: "end_user" }{ approvalRequired: true, extension: "slug", riskLevel: "destructive" }{ needsRunner: true, runnerId: "...", runnerStatus: "offline" }The HTTP status code and response body (truncated)Updating an extension
To ship a new version, POST to the same endpoint with the existing extensionId. A new version is created automatically. Set activate: true to make it live immediately.
curl -X POST https://your-groovy-url/api/extensions \
-H "Content-Type: application/json" \
-b "your-session-cookie" \
-d '{
"extensionId": "EXISTING_EXT_ID",
"agentId": "YOUR_AGENT_ID",
"slug": "acmeops",
"name": "AcmeOps",
"manifest": { ... updated manifest ... },
"activate": true
}'
# A new version is created.
# If activate: true, it becomes the active version immediately.
# If activate: false, the previous version stays live.
# Existing installations automatically use the active version.You can also pin an installation to a specific version by passing versionId in the install call. If not pinned, installations always use the extension's active version.
Finding your agentId
Every Groovy user has an orchestrator agent. The agentId is needed for all extension API calls.
curl https://your-groovy-url/api/orchestrator/agents \
-b "your-session-cookie"
# Response: [{ "id": "YOUR_AGENT_ID", "name": "...", "type": "..." }, ...]
# Use the id of your main orchestrator agent.You can also find it in the dashboard: open the Integrations panel and the agent ID is used automatically.
Complete example: AcmeOps
A full working manifest with 3 tools: list, create, and close incidents.
{
"schemaVersion": 1,
"displayName": "AcmeOps",
"description": "Incident management for Acme operations teams",
"capabilityTags": ["incidents", "ops", "on-call"],
"skillInstructions": "Use AcmeOps tools when the user asks about incidents, outages, on-call, or operational issues. For severity, map urgency words: 'urgent'/'critical'/'P1' → high, 'important' → medium, default → low.",
"tools": [
{
"slug": "list_incidents",
"name": "List Incidents",
"description": "List incidents from AcmeOps, optionally filtered by severity or status",
"riskLevel": "read",
"authScope": "end_user",
"inputSchema": {
"type": "object",
"properties": {
"severity": { "type": "string", "enum": ["low", "medium", "high"] },
"status": { "type": "string", "enum": ["open", "resolved", "all"] }
}
},
"action": {
"kind": "http_action",
"method": "GET",
"url": "{{connection.base_url}}/api/v2/incidents",
"headers": { "Authorization": "Bearer {{connection.api_token}}" },
"query": { "severity": "{{severity}}", "status": "{{status}}" }
}
},
{
"slug": "create_incident",
"name": "Create Incident",
"description": "Create a new incident in AcmeOps",
"riskLevel": "write",
"authScope": "end_user",
"inputSchema": {
"type": "object",
"properties": {
"title": { "type": "string", "description": "Short incident title" },
"severity": { "type": "string", "enum": ["low", "medium", "high"] },
"description": { "type": "string", "description": "Detailed description" }
},
"required": ["title", "severity"]
},
"action": {
"kind": "http_action",
"method": "POST",
"url": "{{connection.base_url}}/api/v2/incidents",
"headers": {
"Authorization": "Bearer {{connection.api_token}}",
"Content-Type": "application/json"
},
"body": {
"title": "{{title}}",
"severity": "{{severity}}",
"description": "{{description}}"
}
}
},
{
"slug": "close_incident",
"name": "Close Incident",
"description": "Resolve and close an incident by its ID",
"riskLevel": "write",
"authScope": "end_user",
"inputSchema": {
"type": "object",
"properties": {
"incident_id": { "type": "string", "description": "Incident ID (e.g. INC-4821)" },
"resolution": { "type": "string", "description": "Resolution summary" }
},
"required": ["incident_id"]
},
"action": {
"kind": "http_action",
"method": "POST",
"url": "{{connection.base_url}}/api/v2/incidents/{{incident_id}}/resolve",
"headers": { "Authorization": "Bearer {{connection.api_token}}" },
"body": { "resolution": "{{resolution}}" }
}
}
]
}Need help? Questions about the platform?
