AI Connect/How to create/Automations & service accounts

Automations & service accounts

Ask AI about Vinkius

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.

Agent loop · one turn
One user turn of the quickstart, exactly as the console serves it. Click a step or press Run.
vinkius.user('alice_123').capabilities({ include: ['github'] })
HTTPGET /apps/vk_app_xxx/users/alice_123/tools?connector=github
const 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
  ...
Step 1 of 6
The agent loop, step by step. Press Run and follow one user turn: capabilities load, convert to tools, the model calls github__create_issue, the SDK executes on that user connection and the result feeds back. Every step shows the real SDK call and its HTTP request.
Real systemsledger · CRM · issuescron · nightlyown external_idwebhookown external_idCI pipelineown external_idstatic credentialsfrom your secret managerbrowsernever neededidempotent actions
Processes as users: headless jobs with their own static credentials act on real systems, idempotent and metered, with no browser anywhere.

What a process-as-user provides

PropertyWhy it matters for unattended jobs
Headless by designStatic-token connectors (api_key, token) need no interactive consent, credentials.set() is the entire flow.
No browser everA cron container with only fetch and your Application key runs the entire SDK.
Per-job blast radiusEach automation's own connection means one exposed key compromises one job, not the estate.
Independent meteringPer-connection tokens mean you see exactly what the nightly-reconcile job costs.
Deterministic retriesAn 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.

typescript
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.

typescript
// 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.

typescript
// 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.

typescript
// 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.

typescript
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.

typescript
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:

typescript
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 idempotencyKey from the business event on every mutating job.
  • [ ] Alert on status() !== 'ready' and isError so 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.

Isolation by construction

Connections and capabilities resolve only inside one external_id. No cross-actor leakage is possible, and you wrote none of that enforcement.

Write-only credentials

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.

Metered, revocable spend

Every connection owns a vk_live_* token, so cost and revocation are per connection. One call to disconnect() is a complete, auditable stop.

Any model runtime

One CapabilitySet converts to OpenAI, Anthropic, Gemini, Vercel AI SDK, LangChain, LlamaIndex, Workers AI or neutral JSON Schema. Only the last line changes.

Production safety built in

idempotencyKey, timeoutMs and AbortSignal per call; automatic full-jitter retries on transient failures; typed VinkiusError branches. No bespoke harness.

Headless and deterministic

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.

Download SKILL.md6 · Available in your language
What unattended automations inherit, plus the SKILL.md, in your language, for your coding agent.

Next steps