Developer Docs

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.

1

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.

Manifest structure
{
  "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 chips
descriptionShort description of your product, shown to admins in the integrations panel
capabilityTagsHelp Groovy find your tools when the user asks relevant questions — acts as a pre-routing index
skillInstructionsNatural language guidance injected into the AI prompt — teach Groovy when to prefer your tools over others
2

Define 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 effects
writeCreates or modifies data
destructiveDeletes or is irreversible
privilegedAdmin / elevated access

Optional 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.
3

Template syntax

Use {{...}} in URLs, headers, query params, request bodies, and CLI args. Values are resolved at execution time.

Available scopes
{{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.
4

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.

Dashboard path

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.

Step 4a — Create extension
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: "..." } }
Step 4b — Install it
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"]
    }
  }'
Step 4c — Save connection auth
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.
5

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.

U

"List all high severity incidents"

Using AcmeOpsext_acmeops_list_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.

Register a runner
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" } }
Install extension with runner
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.

Missing template value
When: A {{...}} reference resolves to undefined
AI sees: Template resolution error with the missing field name
Connection required
When: Tool has authScope ≠ "none" but no connection is saved or the connection status is not "connected"
AI sees: { needsConnection: true, extension: "slug", authScope: "end_user" }
Approval required
When: The install's approval policy includes the tool's riskLevel
AI sees: { approvalRequired: true, extension: "slug", riskLevel: "destructive" }
Runner not online
When: CLI action with a registered runner that is offline or missing
AI sees: { needsRunner: true, runnerId: "...", runnerStatus: "offline" }
API error
When: Your API returns a non-2xx status
AI sees: 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.

Update an existing extension
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.

List your agents
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.

Full AcmeOps manifest
{
  "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?