Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.svantic.com/llms.txt

Use this file to discover all available pages before exploring further.

Embed in Existing Service

Add agent capabilities to an Express app you already have — without restructuring your codebase.
Use agent.start() instead. It handles everything internally.

Why Embed?

You have an existing service — maybe an API, a webhook handler, or a background worker. You want to add AI capabilities without:
  • Rewriting your application structure
  • Running a separate agent process
  • Managing two deployments
attach() lets you keep your existing Express app and add agent capabilities as a layer on top.

Quick Start

import express from 'express';
import { Agent, attach } from '@svantic/sdk';

const app = express();
app.use(express.json());

// Your existing routes stay as-is
app.get('/api/tickets', async (_req, res) => { /* ... */ });

// Create an agent
const agent = new Agent({
  name: 'ticket-agent',
  description: 'Manages support tickets.',
  public_url: 'https://tickets.example.com',
});

// Add capabilities
agent.define_capability({
  name: 'get_ticket',
  description: 'Look up a ticket by ID.',
  parameters: {
    type: 'object',
    properties: {
      ticket_id: { type: 'number' },
    },
    required: ['ticket_id'],
  },
  handler: async (args) => {
    return await db.find(args.ticket_id);
  },
});

// Attach to your app
const handle = await attach(app, {
  agent,
  mesh: {
    client_id: process.env.SAVANT_CLIENT_ID!,
    client_secret: process.env.SAVANT_CLIENT_SECRET!,
  },
});

app.listen(4000);

What attach() Does

One call wires up everything:
  1. A2A endpoints — Mounts /.well-known/agent-card.json (agent discovery) and /send (capability invocation) on your Express app
  2. Mesh registration — Authenticates with the mesh, registers your agent’s capabilities, and starts heartbeating to stay discoverable
  3. Triggers — Activates triggers already registered on the agent via agent.add_triggers()
  4. Graceful shutdown — Listens for SIGTERM and SIGINT to disconnect cleanly before your process exits

Attach Configuration

AttachConfig

agent
object
required
An Agent instance with capabilities already defined. Create with new Agent(\{ name, description, public_url? \}).
mesh
object
Mesh connection credentials. If omitted, the agent runs locally without mesh registration — useful for development and testing.

Triggers

Triggers let external events fire prompts into the mesh. When a trigger fires, the prompt template is filled with payload data and sent to the mesh for processing.

Webhook Triggers

Receive HTTP POST requests from external systems and convert them into mesh prompts. Use this to integrate with Zendesk, Stripe, GitHub, Slack, or any system that sends webhooks.

WebhookTrigger

type
string
required
Trigger type identifier. Must be "webhook".
event
string
required
Event name for logging and identification. Example: "ticket_created".
path
string
required
URL path where the webhook will be mounted. External systems POST to this endpoint. Example: "/webhooks/zendesk".
prompt
string
required
Prompt template sent to the mesh. Use double-brace placeholders for dynamic values. Example: "New ticket \"subject\" (ID: ticket_id). Triage it.".
payload_map
object
Maps payload fields to prompt variables using JSONPath dot-notation.
agent.add_triggers([
  {
    type: 'webhook',
    event: 'ticket_created',
    path: '/webhooks/zendesk',
    prompt: 'New ticket "{{subject}}" (ID: {{ticket_id}}). Triage it.',
    payload_map: {
      ticket_id: '$.ticket.id',
      subject: '$.ticket.subject',
    },
  },
]);
When Zendesk POSTs to /webhooks/zendesk, the payload_map extracts ticket.id and ticket.subject from the JSON body, fills them into the prompt, and sends it to the mesh.

Schedule Triggers

Run prompts on a cron schedule. Use this for daily summaries, periodic checks, or scheduled maintenance tasks.

ScheduleTrigger

type
string
required
Trigger type identifier. Must be "schedule".
event
string
required
Event name for logging and identification. Example: "daily_summary".
cron
string
required
Cron expression (5 fields: minute hour day-of-month month day-of-week). Example: "0 9 * * MON-FRI".
prompt
string
required
Prompt sent to the mesh when the schedule fires. Example: "Summarize all open tickets and send to Slack.".
agent.add_triggers([
  {
    type: 'schedule',
    event: 'daily_summary',
    cron: '0 9 * * MON-FRI',  // 9:00 AM, Monday through Friday
    prompt: 'Summarize all open tickets and send the report to Slack.',
  },
]);
Common cron patterns:
  • 0 9 * * * — Every day at 9:00 AM
  • 0 9 * * MON-FRI — Weekdays at 9:00 AM
  • */15 * * * * — Every 15 minutes
  • 0 0 1 * * — First day of every month at midnight

Emit Triggers

Listen for in-process events fired by your own application code. Use this when your app detects something that needs AI handling — like an SLA warning, a fraud signal, or a customer escalation.

EmitTrigger

type
string
required
Trigger type identifier. Must be "emit".
event
string
required
Event name. Your code fires this event using handle.emit(). Example: "sla_warning".
prompt
string
required
Prompt template sent to the mesh. Use double-brace placeholders. Example: "Ticket ticket_id is approaching SLA breach. Take action.".
// Register the trigger on the agent
agent.add_triggers([
  {
    type: 'emit',
    event: 'sla_warning',
    prompt: 'Ticket {{ticket_id}} is approaching SLA breach. Take action.',
  },
]);

// Fire it from anywhere in your app
handle.emit('sla_warning', { ticket_id: 4521 });

The AgentHandle

attach() returns an AgentHandle for runtime interaction with the mesh.

AgentHandle

emit
function
Fire-and-forget: emit an event that triggers a prompt to the mesh. Does not wait for a response. Signature: (event: string, payload: object) => void.
ask
function
Request-response: emit an event and wait for the mesh to process it. Returns the result. Times out after 30 seconds. Signature: (event: string, payload: object) => Promise<unknown>.
detach
function
Clean shutdown: stops cron timers, removes event listeners, disconnects from mesh, and closes the agent. Call this before your process exits. Signature: () => Promise<void>.
mesh
object
The underlying MeshConnector instance, or null if mesh config was omitted. Use to check connection status or access advanced mesh features.
agent
object
The underlying Agent instance.
const handle = await attach(app, config);

// Fire-and-forget: emit an event, don't wait for response
handle.emit('sla_warning', { ticket_id: 4521 });

// Request-response: emit and wait for the mesh result
const result = await handle.ask('analyze_ticket', { ticket_id: 4521 });
console.log(result);

// Check mesh connection
if (handle.mesh?.connected) {
  console.log('Connected to mesh');
}

// Clean shutdown
await handle.detach();

Full Example

An Express service with existing routes, agent capabilities, and multiple trigger types:
import express from 'express';
import { Agent, attach } from '@svantic/sdk';

const app = express();
app.use(express.json());

// ============================================
// Existing routes — unchanged
// ============================================
app.get('/api/health', (_req, res) => res.json({ ok: true }));

app.get('/api/tickets/:id', async (req, res) => {
  res.json(await db.find(req.params.id));
});

app.post('/api/tickets', async (req, res) => {
  const ticket = await db.create(req.body);
  res.json(ticket);
});

// ============================================
// Agent setup
// ============================================
const agent = new Agent({
  name: 'ticket-service',
  description: 'Ticket management with automatic triage and escalation.',
  public_url: process.env.PUBLIC_URL ?? `http://localhost:${process.env.PORT ?? 4000}`,
});

agent.define_capability({
  name: 'get_ticket',
  description: 'Look up a ticket by ID. Returns subject, status, priority, and history.',
  parameters: {
    type: 'object',
    properties: { 
      ticket_id: { type: 'number', description: 'The ticket ID' } 
    },
    required: ['ticket_id'],
  },
  handler: async (args) => {
    const ticket = await db.find(args.ticket_id);
    if (!ticket) throw new Error(`Ticket ${args.ticket_id} not found`);
    return ticket;
  },
});

agent.define_capability({
  name: 'update_priority',
  description: 'Update the priority of a ticket.',
  parameters: {
    type: 'object',
    properties: {
      ticket_id: { type: 'number', description: 'The ticket ID' },
      priority: { 
        type: 'string', 
        enum: ['low', 'normal', 'high', 'urgent'],
        description: 'New priority level'
      },
    },
    required: ['ticket_id', 'priority'],
  },
  handler: async (args) => {
    return await db.update(args.ticket_id, { priority: args.priority });
  },
});

agent.define_capability({
  name: 'list_open_tickets',
  description: 'List all open tickets, optionally filtered by priority.',
  parameters: {
    type: 'object',
    properties: {
      priority: { 
        type: 'string', 
        enum: ['low', 'normal', 'high', 'urgent'],
        description: 'Filter by priority (optional)'
      },
      limit: { type: 'number', default: 20 },
    },
  },
  handler: async (args) => {
    return await db.list({ status: 'open', ...args });
  },
});

// ============================================
// Register triggers on the agent
// ============================================
agent.add_triggers([
  // Webhook: Zendesk notifies us when a ticket is created
  {
    type: 'webhook',
    event: 'ticket_created',
    path: '/webhooks/zendesk',
    prompt: 'New ticket "{{subject}}" (ID: {{ticket_id}}, priority: {{priority}}). Analyze the content and update priority if needed.',
    payload_map: {
      ticket_id: '$.ticket.id',
      subject: '$.ticket.subject',
      priority: '$.ticket.priority',
    },
  },
  // Schedule: Daily summary every weekday morning
  {
    type: 'schedule',
    event: 'morning_summary',
    cron: '0 9 * * MON-FRI',
    prompt: 'Generate a summary of all open tickets grouped by priority. Send to the #support Slack channel.',
  },
  // Emit: Our code detects SLA risk
  {
    type: 'emit',
    event: 'sla_warning',
    prompt: 'Ticket {{ticket_id}} is {{minutes_remaining}} minutes from SLA breach. Escalate or resolve.',
  },
]);

// ============================================
// Custom trigger handler (optional)
// ============================================
agent.on_trigger = async (ctx) => {
  // ctx has: prompt, trigger, payload, mesh
  // return { skip: true } to block delivery
  // return { prompt: 'override' } to change the prompt
  // return { metadata: {...} } to attach metadata
};

// ============================================
// Attach agent to Express
// ============================================
const handle = await attach(app, {
  agent,
  mesh: {
    client_id: process.env.SAVANT_CLIENT_ID!,
    client_secret: process.env.SAVANT_CLIENT_SECRET!,
  },
});

// ============================================
// Use emit trigger from your own code
// ============================================
app.post('/api/sla-check', async (req, res) => {
  const tickets = await db.list({ status: 'open' });
  
  for (const ticket of tickets) {
    const minutes_remaining = calculate_sla_remaining(ticket);
    if (minutes_remaining < 30) {
      handle.emit('sla_warning', { 
        ticket_id: ticket.id, 
        minutes_remaining 
      });
    }
  }
  
  res.json({ checked: tickets.length });
});

// ============================================
// Start server
// ============================================
const port = Number(process.env.PORT ?? 4000);
app.listen(port, () => {
  console.log(`Ticket service running on port ${port}`);
});

When to Use This

ScenarioRecommendation
Adding AI capabilities to an existing Express appUse attach()
Need webhook or cron triggersUse attach()
Want fire-and-forget or request-response patternsUse attach()
Building a new standalone agentUse agent.start()
Need full control over connection lifecycleUse manual agent.expose(app) + MeshConnector
Local development without meshOmit mesh config — agent runs standalone
attach() is a convenience wrapper. Under the hood it calls agent.expose(app) and creates a MeshConnector — the same primitives you’d use manually.

What’s Next