Automations
Run tools from workflows and background jobs - stateless execution with the same credentials, policies, and traces as your agents - and react to platform events with standing instructions.
Scenario: Not everything happens inside a conversation. Before a support chat starts, Acme enriches the customer from HubSpot. After a call ends, a summary email goes out and the CRM gets updated - from a queue worker, minutes later. Same tools, same per-user credentials, no agent in the loop.
Stateless execution
actions.execute runs one action by slug - no session, no conversation:
const result = await protobox.actions.execute('HUBSPOT_SEARCHCONTACTS', {
actorId: 'user_123', // whose credential to use
arguments: { query: 'initech.com' },
});
if (result.status === 'completed') {
console.log(result.data);
} else if (result.status === 'pending_approval') {
// a toolset policy gated it - see Human-in-the-loop
} else {
console.error(result.error?.code, result.error?.message); // e.g. 'not_connected'
}Every execute lands in the same execution log as agent calls - one audit trail for the whole product.
Find the action and its schema
const actions = await protobox.actions.search({ connectorSlug: 'hubspot', query: 'contact' });
const schema = await protobox.actions.getSchema('HUBSPOT_SEARCHCONTACTS');
// schema.inputSchema - validate your job payloads against it in CIDry-run from CI
dryRun: true validates arguments and resolves the credential without calling the provider - perfect for smoke-testing a workflow deploy:
const check = await protobox.actions.execute('ACME_CREATETICKET', {
actorId: 'user_123',
arguments: { subject: 'smoke' },
dryRun: true,
});Relay your own context
context.forward is carried verbatim as an X-Protobox-Context header to connectors that opted in (forwardContext: true at registration) - so your API can correlate tool calls to your conversations without Protobox parsing anything:
await protobox.actions.execute('ACME_CREATETICKET', {
actorId: 'user_123',
arguments: { subject: 'Renewal follow-up' },
context: { forward: JSON.stringify({ conversationId: 'c_991' }) },
});The three moments
| Moment | Pattern |
|---|---|
| Before an interaction | Enrich at boot: a few read-only actions.execute calls, results into your prompt/context |
| During | The agent path - sessions or adapters |
| After | Queue worker: write-backs, follow-up email, ticket updates - gated by approvals where they're risky |
Reacting to events
The platform pushes its own events to your webhook - connection lifecycle and execution outcomes (reference). The high-leverage automation pair:
switch (event.type) {
case 'connection.activated':
// kick off a first-sync job for this user's newly connected app
await enqueueFirstSync(event.data.callerId, event.data.integrationId);
break;
case 'execution.failed':
// alert on failures from unattended jobs
await notifyOncall(event.data.executionId);
break;
}Standing instructions with automations
protobox.automations (alias: protobox.triggers - same instance) is a persisted "when <event> happens, run <tool|skill>" rule. Every run goes through the SAME quota, permission, policy, and approval gates a live call gets - an automation that would need approval pauses into your Approvals inbox exactly like an agent call would.
const automation = await protobox.automations.create({
name: 'New GitHub issue -> Slack summary',
trigger: { kind: 'webhook', source: 'github', eventType: 'issues.opened' },
action: {
kind: 'tool',
name: 'slack_send_message',
argsTemplate: { channel: 'C0123', text: 'New issue: {{event.payload.title}}' },
},
budget: { maxRunsPerDay: 50 },
});
// webhook triggers get a signing secret, shown ONLY here - send events to
// POST /api/v1/automations/inbound/<automation.id>, HMAC-signed with it
// (X-Automation-Signature: t=<unix>,v1=<hex>, same scheme as the platform's
// own outbound webhooks - see webhooks.verify).
console.log(automation.secret);Three trigger kinds:
| Kind | Fires when | Example |
|---|---|---|
webhook | A signed event lands on POST /automations/inbound/:id matching source/eventType | A GitHub/Zapier/your-own-backend event |
schedule | A 5-field cron expression matches the current UTC minute | { cron: '0 9 * * *' } - daily at 9am |
knowledge.updated / memory.stored | A document is added/updated, or a memory is stored, in this workspace | Re-summarize a KB doc on every edit |
Three action kinds. A tool action renders argsTemplate's {{event.x}} tokens from the trigger event and executes the tool directly; a skill action renders inputTemplate the same way, resolves the named skill's prompt content with those variables, stores the rendered text as the run's output, and sends you an in-app notification; an agent action takes the only real LLM turn of the three.
const runs = await protobox.automations.runs(automation.id);
// [{ id, status: 'done' | 'pending_approval' | 'failed' | 'budget_stopped', executionId, output, ... }]budget.maxRunsPerDay caps runs per UTC calendar day - a run created at/over the cap is recorded with status: 'budget_stopped' and never reaches the executor, so a misconfigured or noisy trigger can't spend past the cap.
Headless agent turns
{ kind: 'agent', instructions, toolsetId?, maxSteps?, budget: { maxCentsPerDay } } gives the event to a bounded model turn instead of a fixed tool call - Protobox decides what to do, not you:
const triage = await protobox.automations.create({
name: 'Triage new tickets',
trigger: { kind: 'webhook', source: 'zendesk', eventType: 'ticket.created' },
action: {
kind: 'agent',
instructions: 'A support ticket just came in: "{{event.payload.subject}}". Look up the customer and post a triage summary to Slack.',
toolsetId: 'ts_support', // the tools this turn may call - same approval gates as the toolset's tool/skill actions
maxSteps: 4, // hard-capped at 5
budget: { maxCentsPerDay: 100 },
},
budget: { maxRunsPerDay: 100 },
});- Every tool the model calls goes through the same
ToolExecutorService.execute(..., 'automation')path atoolaction gets - quota, permission, policy, and approval all apply. A toolsetapprovepolicy pauses the RUN intopending_approvalthe instant the gated call happens; the turn takes no further steps. - The model comes from your workspace's engine policy, same as memory and knowledge: a BYO OpenAI key (Integrations) runs on any tier; without one, paid tiers run on a chanl-managed key and free-tier runs are refused - the run is recorded
status: 'failed'with anUPGRADE_REQUIREDerror, never silently skipped. action.budget.maxCentsPerDayis a separate cap frombudget.maxRunsPerDay- it bounds this action's real provider spend specifically. Once today's spend for the automation already meets the cap, the next run is recordedbudget_stoppedbefore the model is ever called.- The run's
transcript({ steps, toolCalls, finalText }) andcostCentsrecord what happened - checkrun.transcript.toolCallsto see exactly what the model did.
const [run] = await protobox.automations.runs(triage.id);
console.log(run.transcript?.finalText, run.costCents);Production notes
- Retries are yours. The SDK makes exactly one attempt per call - no built-in retry or backoff. Run executes from a queue with your retry policy; on
rate_limitederrors, honorerror.retryAfter. - Idempotency:
idempotencyKeyis accepted onexecutebut not enforced yet (it arrives with a later phase). Until then, make write actions safe to repeat at your layer - dedupe by your job id before enqueueing. - Multi-account actors: pass
accountKeyalongsideactorIdto pick which of the user's accounts the job runs as.
Related
- Your API as agent tools - the actions your jobs will call most
- Human-in-the-loop - approvals pause background writes too
Use Protobox from your own runtime (REST, no MCP client)
Drive one session per conversation entirely through @protoboxai/sdk over REST: list tools, execute actions, resolve approvals, and read the same audit trail an MCP client would, with no MCP handshake anywhere.
Ask from Slack
Wire a `/ask` slash command to a public knowledge pack, so your team gets a cited answer without leaving Slack.