Source: docs/integration/capability-quickstart.md

Capability quickstart — Your first read capability

This guide walks you through validating and publishing a read-only business

capability end-to-end. By the end, your agent will call your endpoint to answer

user questions with live data.

Time: ~15 minutes

Prerequisites: A Forgium API key and an HTTPS endpoint (or local development server)

Overview

Scaffold → Implement → Preflight → Credential → Import (active) → Test → Chat

Step 1: Generate the scaffold

Use the CLI to generate a read-only capability scaffold:

bun run capability:scaffold -- --domain people --name lookup_person --out ./my-capability

This creates:

Step 2: Implement your endpoint

Edit endpoint.js and replace the placeholder logic with your actual business

logic. The endpoint must:

Example implementation:

app.post("/v1/lookup_person", (req, res) => {
  const { query } = req.body;

  // Your business logic here
  const result = lookupPerson(query);

  // Return only fields defined in output_schema
  res.json({
    id: result.id,
    result: result.name
  });
});

Step 3: Test locally

Run the local test to verify your endpoint works:

cd ./my-capability
node test-local.js

Expected output:

🧪 Running tests on http://localhost:XXXXX

1. Testing health check...
   ✅ Health check passed

2. Testing capability with valid input...
   ✅ Capability response structure valid

3. Testing capability with invalid input...
   ✅ Invalid input correctly rejected

✅ All tests passed!

Step 4: Update the manifest

Edit manifest.json and replace the placeholder values:

  1. URL: Replace https://your-endpoint.example.com/v1/lookup_person
    with your real endpoint URL
  1. Schemas: Update input_schema and output_schema to match your
    actual data structure
  1. Examples: Add real user messages and tool inputs
  1. Business rules: Update with your actual business rules

Step 5: Validate with the linter

Run the linter to check your manifest:

bun run capability:lint -- ./my-capability/manifest.json

The linter checks for:

Expected output for a valid manifest:

✓ Manifest is valid

If there are errors, fix them before proceeding.

Step 6: Provision your endpoint credential

Store your endpoint token in the credential vault. The token is encrypted

before storage and never appears in logs or API responses:

curl -X PUT "$FORGIUM_AGENT_BASE_URL/v1/capability-credentials/cred_people_api" \
  -H "Authorization: Bearer $FORGIUM_AGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"auth_type":"bearer","secret":"YOUR_ENDPOINT_TOKEN"}'

Expected response:

{
  "credential_ref": "cred_people_api",
  "status": "configured"
}

Step 7: Import your manifest

You can create a capability in two ways: single (recommended for adding one

capability) or import (for bulk creation of multiple capabilities at once).

Option A: Create a single capability

curl -X POST "$FORGIUM_AGENT_BASE_URL/v1/capabilities" \
  -H "Authorization: Bearer $FORGIUM_AGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: create-people-$(date +%s)" \
  -d '{
    "domain": "people",
    "capability": {
      "name": "lookup_person",
      "description": "Looks up a person by name or email.",
      "when_to_use": "Use when a user asks for person details.",
      "method": "POST",
      "url": "https://your-endpoint.example.com/v1/lookup_person",
      "auth": { "type": "bearer", "credential_ref": "cred_people_api" },
      "safe_headers": { "accept": "application/json", "content-type": "application/json" },
      "input_schema": { ... },
      "output_schema": { ... },
      "business_rules": ["Your backend enforces authorization."],
      "examples": [],
      "side_effect": "read",
      "requires_approval": false,
      "timeout_ms": 5000,
      "retry_count": 1
    }
  }'

Expected response:

{
  "id": "cap_...",
  "lifecycle_status": "active",
  "revision": 1
}

Option B: Import a manifest (bulk)

Import the manifest to validate and publish one or more active capabilities:

curl -X POST "$FORGIUM_AGENT_BASE_URL/v1/capabilities/import" \
  -H "Authorization: Bearer $FORGIUM_AGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: import-people-$(date +%s)" \
  --data-binary @./my-capability/manifest.json

Expected response:

{
  "capabilities": [
    {
      "id": "cap_...",
      "lifecycle_status": "active",
      "revision": 1
    }
  ]
}

Save the id — you'll need it for the next steps.

Step 8: Run the contract test

Test your capability with a sample input:

curl -X POST "$FORGIUM_AGENT_BASE_URL/v1/capabilities/$CAPABILITY_ID/test" \
  -H "Authorization: Bearer $FORGIUM_AGENT_API_KEY" \
  -H "If-Match: 1" \
  -H "Content-Type: application/json" \
  -d '{"arguments":{"query":"test query"}}'

Expected response:

{
  "status": "passed",
  "capability_id": "cap_...",
  "revision": 1
}

If the test fails, check your endpoint logs and fix any issues.

Step 9: Manage published state

Import already publishes the validated capability. Disable it explicitly when

the endpoint must stop receiving traffic:

curl -X POST "$FORGIUM_AGENT_BASE_URL/v1/capabilities/$CAPABILITY_ID/disable" \
  -H "Authorization: Bearer $FORGIUM_AGENT_API_KEY" \
  -H "If-Match: 1"

Expected response:

{
  "id": "cap_...",
  "lifecycle_status": "disabled",
  "revision": 2
}

Re-enable it only through the explicit enable operation. State changes and

revisions require the current If-Match value:

curl -X POST "$FORGIUM_AGENT_BASE_URL/v1/capabilities/$CAPABILITY_ID/enable" \
  -H "Authorization: Bearer $FORGIUM_AGENT_API_KEY" \
  -H "If-Match: 2"

An update uses PATCH with If-Match as well. It validates the complete new

manifest and publishes it atomically; it never creates a draft or silently

re-enables a disabled capability. Capability IDs are immutable, and each

successful update or state transition increments its revision.

Step 10: Verify with a chat message

Send a test message through the HTTP channel:

curl -X POST "$FORGIUM_AGENT_BASE_URL/v1/channels/http/messages" \
  -H "Authorization: Bearer $FORGIUM_AGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": {
      "type": "text",
      "body": "What is the email of Juan Pérez?"
    }
  }'

The agent should call your capability and return a response based on your

endpoint's data.

Preparing a host-owned operation

Public capabilities are read-only. If the host needs to continue with a

confirmation or selection, register a read-only preparation capability with

result_handling: "host_interaction" and the closed output schema described

in the canonical host-interaction output schema.

Forgium returns the bounded interaction result but does not execute the

operation. The host application owns confirmation, authorization, idempotency,

concurrency, and the eventual mutation.

Treat the preparation endpoint as a just-in-time preflight. Forgium calls it

after the user expresses an operation intent; the host does not need to predict

that intent in advance. Resolve matching resources and check their current

eligibility in deterministic host code. Return not_found when nothing

matches or when matches exist but the requested operation is not currently

available. If preparation succeeds, repeat authorization and state checks at

mutation time because the resource may change before confirmation.

Environment-specific configuration

Local development

For local development, you can use localhost URLs:

{
  "url": "http://localhost:3000/v1/lookup_person"
}

Note: localhost URLs cannot be called by deployed Workers. Use them only

for local testing.

Staging

For staging, use your staging endpoint URL:

{
  "url": "https://staging-api.example.com/v1/lookup_person"
}

Production

For production, use your production endpoint URL:

{
  "url": "https://api.example.com/v1/lookup_person"
}

Schema requirements

All schemas must follow these rules:

description is an annotation. Every other keyword is enforced by the

runtime. All keywords not listed for the relevant schema are rejected at

import, including allOf, $ref, minimum, maximum, minItems, and

format. Composition keywords are intentionally input-only: output

schemas must retain one explicit shape so Forgium can safely remove fields not

declared in the schema.

Constrain a bounded string format

pattern is supported only in input_schema string fields that declare

maxLength. It uses ECMAScript regular-expression syntax and is enforced

before the capability endpoint is called. For example, an optional periodo

argument in YYYY-MM format can reject invalid months:

{
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "periodo": {
      "type": "string",
      "maxLength": 7,
      "pattern": "^\\d{4}-(0[1-9]|1[0-2])$",
      "description": "Optional accounting period in YYYY-MM format."
    }
  }
}

pattern is not supported in output_schema. It improves argument quality

but does not replace endpoint validation, authorization, or business rules;

the capability backend remains authoritative.

Require exactly one selector (XOR)

Use oneOf when a capability needs exactly one of two alternative arguments.

The following input_schema accepts either unidad_numero or

persona_nombre, but rejects a request that provides neither or both:

{
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "unidad_numero": { "type": "string", "maxLength": 32 },
    "persona_nombre": { "type": "string", "maxLength": 200 }
  },
  "oneOf": [
    { "required": ["unidad_numero"] },
    { "required": ["persona_nombre"] }
  ]
}

anyOf is also supported for inclusive alternatives. To express the same XOR

without oneOf, combine it with not:

{
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "unidad_numero": { "type": "string", "maxLength": 32 },
    "persona_nombre": { "type": "string", "maxLength": 200 }
  },
  "anyOf": [
    { "required": ["unidad_numero"] },
    { "required": ["persona_nombre"] }
  ],
  "not": {
    "required": ["unidad_numero", "persona_nombre"]
  }
}

Each anyOf or oneOf array must contain between one and eight bounded

schemas.

Security checklist

Troubleshooting

Linter rejects my manifest

Run the linter with --format json to see detailed diagnostics:

bun run capability:lint -- ./my-capability/manifest.json --format json

Contract test fails

Check your endpoint logs for errors. Common issues:

Capability not called by agent

Verify:

Next steps