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:
manifest.json— Capability manifest with placeholder valuesendpoint.js— Reference endpoint implementationtest-local.js— Local test scriptREADME.md— Instructions and next steps
Step 2: Implement your endpoint
Edit endpoint.js and replace the placeholder logic with your actual business
logic. The endpoint must:
- Accept POST requests with JSON body
- Validate input against your
input_schema - Return only fields defined in
output_schema - Enforce your own authorization and business rules
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:
- URL: Replace
https://your-endpoint.example.com/v1/lookup_person
with your real endpoint URL
- Schemas: Update
input_schemaandoutput_schemato match your
actual data structure
- Examples: Add real user messages and tool inputs
- 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:
- Placeholder values (must be replaced)
- Invalid URLs
- Secret-shaped values
- Unsupported schema keywords
- Missing schema bounds
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:
- Objects: Must have
additionalProperties: false - Strings: Must have
maxLength - Arrays: Must have
maxItems input_schemakeywords:type,properties,required,additionalProperties,items,maxItems,minLength,maxLength,enum,const,description,anyOf,oneOf,not,patternoutput_schemakeywords:type,properties,required,additionalProperties,items,maxItems,minLength,maxLength,enum,const,description
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
- [ ] Never commit tokens, API keys, or secrets
- [ ] Use environment variables or secret managers for credentials
- [ ] The manifest should never contain sensitive values
- [ ] Validate all input at your endpoint
- [ ] Return only fields safe for model context
- [ ] Enforce your own authorization and business rules
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:
- Invalid input validation
- Missing required fields in response
- Response doesn't match
output_schema
Capability not called by agent
Verify:
- Capability is
active(notdisabled) - Credential is
configured - Endpoint URL is accessible from Forgium Workers
- Input schema matches the agent's expected arguments
Next steps
- Self-service business capabilities — Full lifecycle reference
- Forgium-Context verification — Validate signed requests at your endpoint
- Agent policy — Configure response instructions and topic scope
- Error reference — Common error codes and fixes