AI Connect/How to create/Automations & service accounts
Automations & service accounts
Let cron jobs, webhooks, CI pipelines and nightly batch processes own their own AI Connect SDK user, connect systems headlessly with static credentials, and act with per-job isolation, budgets and no browser anywhere. Processes become governed actors, not silent keys in a config file.
The most valuable AI in your company runs unattended, without a human in the loop. A nightly job that reconciles the ledger, a webhook that triages a new lead into the CRM, a CI step that opens an issue on a failed deploy. Until today, infrastructure had exactly one shape for that work: one god-key in an env file, no identity, no metering, no revocation, and a security review that ends in a shrug.
The AI Connect SDK replaces that shape with what no connectivity platform has offered before: a process is a user, with its own external_id, its own connectors, its own static credentials, backed by thousands of AI connections from day one. Give each automation its own identity, connect it once, and let it call capabilities indefinitely, isolated, metered and auditable. Every cron job becomes an accountable employee with a badge, a budget and its own offboarding. That is a sentence you could not write about any integration platform on the market until this one.
vinkius.user('alice_123').capabilities({ include: ['github'] })GET /apps/vk_app_xxx/users/alice_123/tools?connector=githubconst capabilities = await vinkius
.user('alice_123')
.capabilities({ include: ['github'] });CapabilitySet (6)
github__list_issues read-only
github__create_issue POST /repos/{owner}/{repo}/issues
github__list_pull_requests read-only
github__search_code read-only
...What a process-as-user provides
| Property | Why it matters for unattended jobs |
|---|---|
| Headless by design | Static-token connectors (api_key, token) need no interactive consent, credentials.set() is the entire flow. |
| No browser ever | A cron container with only fetch and your Application key runs the entire SDK. |
| Per-job blast radius | Each automation's own connection means one exposed key compromises one job, not the estate. |
| Independent metering | Per-connection tokens mean you see exactly what the nightly-reconcile job costs. |
| Deterministic retries | An idempotency key guarantees a repeated call is applied exactly once. |
These jobs hold real credentials with no human in the loop. Keep every snippet server-side, source static tokens from your secret manager (never a repository or a model prompt), and give each automation the smallest connector set it can function with.
1. One automation, one id, one connector set
Name the job by what it does, and treat that name as the actor that owns the connections.
const JOB = 'svc-nightly-reconcile'; // stable, URL-safe, under 255 chars, no / \ or spaces
const JOB_CONNECTORS = ['netsuite', 'stripe', 'sheets'];2. Provision a service account (one time, at setup)
This runs once during onboarding, an operator or a bootstrap script supplies the static tokens. After that, the job only uses the connection.
// scripts/bootstrap-reconcile.ts
import { Vinkius } from '@vinkius/connect';
const vinkius = new Vinkius({
appId: process.env.VINKIUS_APP_ID!,
apiKey: process.env.VINKIUS_APP_KEY!, // from the secret manager
});
async function provision(jobId: string, tokens: Record<string, Record<string, string>>) {
const user = vinkius.user(jobId);
await user.ensure({ kind: 'service-account', job: 'nightly-reconcile' });
for (const [slug, values] of Object.entries(tokens)) {
const connector = user.connector(slug);
await connector.connect(); // get-or-create, idempotent
await connector.credentials.set(values); // write-only; e.g. { API_KEY: … }
}
return Promise.all(
Object.keys(tokens).map(async (slug) => ({
slug,
status: await user.connector(slug).status(), // expect "ready"
})),
);
}
await provision('svc-nightly-reconcile', {
netsuite: await secrets.read('netsuite.reconcile'),
stripe: await secrets.read('stripe.reconcile'),
sheets: await secrets.read('sheets.reconcile'),
});credentials.set() never returns the stored values, and status() only reports which keys are configured. A process can verify its connectors are ready without ever being able to exfiltrate what it was given, the credential is usable, not readable.
3. The unattended job itself
The scheduled process needs no browser, no consent, no user present. It loads its capabilities and acts.
// jobs/nightly-reconcile.ts
import { Vinkius } from '@vinkius/connect';
const vinkius = new Vinkius({
appId: process.env.VINKIUS_APP_ID!,
apiKey: process.env.VINKIUS_APP_KEY!,
});
async function run() {
const job = vinkius.user('svc-nightly-reconcile');
const capabilities = await job.capabilities({ include: ['netsuite', 'stripe'] });
const fetchOpen = capabilities.findCapability('stripe__list_invoices');
const postEntry = capabilities.findCapability('netsuite__create_journal_entry');
if (!fetchOpen || !postEntry) {
await alertOps('reconcile: a capability is unavailable, did a credential expire?');
return;
}
const invoices = await fetchOpen.execute({ status: 'open', limit: 200 });
if (invoices.isError) throw new Error('stripe list failed: ' + invoices.content[0]?.text);
const entry = await postEntry.execute(
{ lines: toJournalLines(invoices) },
{ idempotencyKey: `reconcile:${runDate()}` }, // one logical run = one entry
);
if (entry.isError) await alertOps('reconcile entry rejected: ' + entry.content[0]?.text);
}
run().catch(async (error) => {
await alertOps(`reconcile crashed: ${error.message}`);
});idempotencyKey is the automation feature. A retry, a duplicate cron fire, a re-deploy mid-run, none of them post the journal entry twice, because the server deduplicates replays carrying the same key. Derive the key from the business event (the run date, the ticket id, the webhook delivery id), never from Date.now().
4. Webhooks: one actor, one delivery per key
For event-driven automations you usually keep one service-account user, but every mutating call is keyed by the delivery id so a retried webhook is applied exactly once.
// POST /webhooks/lead (verified)
async function handleLeadWebhook(payload: { id: string; email: string }) {
const user = vinkius.user('svc-lead-intake');
if ((await user.connector('hubspot').status()) !== 'ready') {
await alertOps('lead-intake CRM not ready');
return;
}
const caps = await user.capabilities({ include: ['hubspot'] });
await caps.findCapability('hubspot__create_contact')?.execute(
{ email: payload.email },
{ idempotencyKey: `lead:${payload.id}` }, // retried delivery -> no duplicate
);
}5. CI pipelines and one-shot runners
A CI job authenticates exactly like a cron: same Application key, its own external_id, static credentials provisioned in the environment. The difference is lifetime, you disconnect() ephemeral runners when the pipeline is decommissioned.
async function openIssueOnFailedDeploy(runId: string, repo: string) {
const caps = await vinkius.user('ci-deploy-bot').capabilities({ include: ['github'] });
await caps.findCapability('github__create_issue')?.execute(
{ owner: 'acme', repo, title: `Deploy ${runId} failed` },
{ idempotencyKey: `deploy:${runId}` },
);
}6. Rotate and retire cleanly
Because everything depends on a single external_id, decommissioning an automation is deterministic.
async function decommission(jobId: string) {
const user = vinkius.user(jobId);
for (const conn of await user.connectors()) {
await user.connector(conn.slug).disconnect();
}
}To rotate a credential, call credentials.set() again with the new value, the connection stays the same and no capability references break.
Reliability and structured output, for unattended processes
An unattended process is where the SDK's built-in robustness matters most. Three guarantees are inherited automatically:
Automatic retry with thundering-herd protection. Idempotent reads (and any write carrying an idempotencyKey) are retried automatically: but only for transient 429/502/503/504 and network errors, with full-jitter backoff that honors a server Retry-After. Your cron does not need its own retry loop.
Idempotency is the safety mechanism. In a batch job, a failure and re-run are routine. A stable idempotencyKey removes the ambiguity of whether the re-run duplicated the effect, because the server deduplicates replays on that key. Supply the key on a mutating call and the SDK will safely retry even a non-idempotent POST.
Consume results as objects, not text. When a connector returns structured data, result.structuredContent provides it already parsed, with no fragile text extraction in the pipeline:
const invoices = await fetchOpen.execute({ status: 'open', limit: 200 });
const list = invoices.structuredContent as { invoices: Array<{ id: string; amount: number }> };
const total = list.invoices.reduce((sum, i) => sum + i.amount, 0);Pair long batch runs with an appropriate per-call timeoutMs and a signal aborted on process shutdown, and the job terminates predictably during a progressive deploy.
Production checklist
- [ ] Give each automation a distinct, human-readable
external_id(no single broadly shared account). - [ ] Source every static token from your secret manager; never commit or prompt it.
- [ ] Provision once at setup; runtimes only read
status(), they never re-store secrets. - [ ] Set a stable
idempotencyKeyfrom the business event on every mutating job. - [ ] Alert on
status() !== 'ready'andisErrorso a silent failure is not an invisible failure. - [ ]
disconnect()ephemeral service accounts as part of teardown.
You now have automations that own their own identity, connect real systems headlessly, and act on a schedule or an event with none of the OAuth mechanics and none of the "who owns this token?" drift of a broadly shared account. Your unattended estate went from the part of the system audits complain about to the part with the best governance story, because every process in it finally has a name.
What you just got
Not a pitch: the properties this build inherits automatically.
Connections and capabilities resolve only inside one external_id. No cross-actor leakage is possible, and you wrote none of that enforcement.
Your server stores secrets and can read back which fields are configured, never the values. Not your code, the model, or a dashboard can exfiltrate them.
Every connection owns a vk_live_* token, so cost and revocation are per connection. One call to disconnect() is a complete, auditable stop.
One CapabilitySet converts to OpenAI, Anthropic, Gemini, Vercel AI SDK, LangChain, LlamaIndex, Workers AI or neutral JSON Schema. Only the last line changes.
idempotencyKey, timeoutMs and AbortSignal per call; automatic full-jitter retries on transient failures; typed VinkiusError branches. No bespoke harness.
A cron container with only fetch and your key runs it. An idempotencyKey from the business event makes a re-run a non-event.
Give it to your AI agent
An Agent Skill (SKILL.md) for this build. Preview the first lines below, then copy or download it into your repo under .claude/skills/: Claude Code, Cursor or any Agent-Skills-compatible agent follows it to implement this pattern correctly.
