Recipes

Copy-pasteable patterns for common agent flows. Each assumes you've run am init and have ANIMA_API_KEY in your env. For setup, see /docs.

Inbound email triage → voice callback

Customer emails support. Subject contains 'urgent'. Call them back automatically.

import { Anima } from '@anima/sdk';
const am = new Anima({ apiKey: process.env.ANIMA_API_KEY! });

am.on('email.received', async (msg) => {
  if (!msg.subject.toLowerCase().includes('urgent')) return;
  if (!msg.from.phone) {
    await am.email.send({
      threadId: msg.threadId,
      to: msg.from.email,
      subject: `Re: ${msg.subject}`,
      html: '<p>Got it — calling you back. Could you share your phone number?</p>',
    });
    return;
  }
  await am.voice.placeCall({
    correlationId: msg.correlationId, // threads with the email
    to: msg.from.phone,
    consentSource: 'customer-initiated',
    greeting: `Hi, calling about your "${msg.subject}" email.`,
  });
});

Why it works: The correlationId from the inbound email threads through to the voice call, so /audit shows them as one chain. consentSource = 'customer-initiated' satisfies the TCPA gate because they emailed us first.

Scheduled outbound (cron + tier check)

Send a weekly digest only if the agent has the email quota.

// Run from your scheduler (Cloud Scheduler, Vercel Cron, etc.)
const am = new Anima({ apiKey: process.env.ANIMA_API_KEY! });
const usage = await am.billing.getUsage();
const remainingEmail = usage.tierIncluded.email - usage.used.email;
if (remainingEmail < 50) {
  console.log('Skipping digest — under monthly quota');
  return;
}
const subscribers = await loadSubscribers();
for (const sub of subscribers) {
  await am.email.send({
    to: sub.email,
    subject: 'Your weekly digest',
    html: render(sub),
    // Idempotent retries: same key = same response if network drops
    idempotencyKey: `digest-${sub.id}-${weekStart()}`,
  });
}

Why it works: The Idempotency-Key matches per-subscriber per-week. If the cron retries (network blip, container restart), the digest doesn't double-send. The hard-cap at 90% would email-warn the operator before this script gets close to the cap.

Vault credential rotation

Rotate a stored API key when the upstream service notifies via webhook.

// Webhook handler — verifies signature elsewhere
async function rotateCredential(serviceId: string, newSecret: string) {
  // Read the current credential first (audit trail)
  const current = await am.vault.read({ serviceId, agentId: AGENT_ID });
  // Update with the new secret
  await am.vault.update({
    credentialId: current.id,
    fields: { api_key: newSecret },
  });
  // The vault stores ciphertext; the agent gets plaintext on next read.
  // /audit?correlation_id=... will show one chain:
  //   webhook.received → vault.credential.read → vault.credential.write
}

Why it works: Vault writes are server-side encrypted before storage. The audit chain captures the rotation as one workflow. If the upstream rotation fails, you can replay the webhook with the same Idempotency-Key without double-rotating.

Cross-channel audit query

Customer support asks: 'what did agent X do for tenant Y today?'

// Fetch every event for an agent in the last 24h, grouped by correlation
const events = await am.audit.list({
  agentId: 'agent_8x1',
  since: new Date(Date.now() - 24 * 60 * 60 * 1000),
});

const chains = new Map<string, typeof events>();
for (const evt of events) {
  const id = evt.correlationId;
  if (!chains.has(id)) chains.set(id, []);
  chains.get(id)!.push(evt);
}

// chains is now a Map<workflow-id, ordered-events>
for (const [id, chain] of chains) {
  console.log(`${id}: ${chain.length} events across ${new Set(chain.map(e => e.channel)).size} channels`);
}

Why it works: Every cross-channel action emits a row to audit_events keyed by correlationId. Grouping by ID gives you the workflow view; grouping by agentId gives you the per-agent view. The dashboard /audit page does the same query with a UI on top.

Hard-cap alert-only for production agent

Voice meter blocks at 100% by default. For a production support agent, alert-only is safer than mid-call hangup.

// Run once via the dashboard or this admin endpoint
await am.billing.updateHardCap({
  meters: {
    voice: { mode: 'alert-only', capCents: 100000 }, // $1,000/mo
  },
});

// Now: 90% triggers email warning. 100% emits a notification event
// but does NOT block the next call. The operator gets paged but
// the customer call goes through.

Why it works: Default mode 'block' is right for most agents — better to refuse a marginal call than overspend. alert-only is the escape hatch for agents where a mid-call hangup is worse than the overage. Per-meter, so email can stay 'block' while voice is 'alert-only'.

Have a pattern you'd like to see here? Tell us.

support@useanima.sh