Automation SDK
Instrument external Node.js automations and native Playwright jobs with the private SDK.
On this page
The private TypeScript automation SDK adds structured BernOps telemetry to external Node.js automations and BernOps-managed Playwright jobs. This guide covers the supported 0.1.0-internal.3 artifact. The package is UNLICENSED and is not published to a public registry. External consumers install the exact retained release bundle; deployed native jobs use the exact artifact provisioned by their BernOps worker operator.
Prerequisites and installation
- Use Node.js 22 or 24 for external automations and local native-job authoring. Node.js 24 is recommended.
- For an external automation or local type-checking, ask your BernOps release owner for the matching
.tgz,.tgz.sha256, and.manifest.jsonfiles. Keep the three files together. - Import only from
@bernops/automation. ESM, CommonJS, and TypeScript consumers are supported; deep imports are not.
Verify the checksum before installing the artifact into an external automation or local authoring project:
export SDK_ARTIFACT_DIR=/absolute/path/to/retained-candidate
export SDK_TARBALL="$SDK_ARTIFACT_DIR/bernops-automation-0.1.0-internal.3.tgz"
cd "$SDK_ARTIFACT_DIR"
sha256sum --check "$(basename "$SDK_TARBALL").sha256"
cd /absolute/path/to/your-automation
npm install --offline --ignore-scripts --no-audit --no-fund \
--registry=https://registry.invalid/ "$SDK_TARBALL"
npm ls @bernops/automationThe SHA-256 sidecar must come from the same trusted release as the manifest. A release owner with the matching BernOps source revision can run the complete manifest, package-content, declaration, and clean-consumer gate:
cd /absolute/path/to/bernops/sdk/typescript
npm run artifact:verify -- --manifest \
"$SDK_ARTIFACT_DIR/bernops-automation-0.1.0-internal.3.manifest.json"For external and local projects, do not replace the tarball with a workspace link, source-directory install, mutable alias, deep import, or public registry fallback. A deployed native job does not use your local node_modules; its worker provisions the package. If either artifact version differs from this guide, use the matching documentation and compatibility policy instead of assuming compatibility.
Choose a transport
| Where the automation runs | Transport | Credential | Lifecycle owner |
|---|---|---|---|
| CI, scheduler, service, or script outside a BernOps worker | external | Job-scoped ingest-runner API key | SDK |
| Playwright job dispatched by a BernOps worker | native | Worker-injected loopback token; no API key | Worker |
Select the transport explicitly with BernOps.fromEnv({ transport: 'external' }) or BernOps.fromEnv({ transport: 'native' }). Use auto only when the deployment guarantees exactly one complete configuration. Selection fails closed and never falls back after authentication, TLS, identity, or capability errors.
External quick start
Create an external job and an ingest-runner key, then store the plaintext key in the process secret manager. The minimal writer scopes are runs:write, runs:finish, logs:write, steps:write, metrics:write, progress:write, and attachments:write. Read and admin scopes are not required by the SDK writer.
export BERNOPS_BASE_URL=https://bernops.example
export BERNOPS_API_KEY='job-scoped-secret'
export BERNOPS_EXTERNAL_JOB_ID='external-job-uuid'
export AUTOMATION_RUN_ID='scheduler-attempt-123'Set AUTOMATION_RUN_ID from a durable scheduler, CI-system, or business-attempt ID. Reuse that value when the same attempt is retried; do not generate a new ID on each process start.
import { BernOps, type DeliveryReport } from '@bernops/automation';
function requiredEnv(name: string): string {
const value = process.env[name]?.trim();
if (!value) throw new Error(`Missing required environment variable: ${name}`);
return value;
}
function deliveryIsDegraded(report: DeliveryReport): boolean {
return report.pending > 0 || report.failed > 0 ||
report.dropped > 0 || report.timedOut;
}
const client = BernOps.fromEnv({
transport: 'external',
redaction: { secretEnvironmentVariables: ['CUSTOM_SERVICE_TOKEN'] },
onDiagnostic(diagnostic) {
// The callback receives bounded, already-redacted SDK diagnostics.
console.warn(`[bernops:${diagnostic.code}] ${diagnostic.message}`);
},
});
try {
await client.capabilities();
await client.run(
{
externalRunId: requiredEnv('AUTOMATION_RUN_ID'),
name: 'nightly import',
service: 'catalog',
environment: 'production',
},
async (run) => {
const admission = run.log.info('import started');
if (!admission.accepted) {
console.warn(`BernOps event was not queued: ${admission.reason}`);
}
await run.step('import records', async () => {
run.metric.count('records.imported', 250, { unit: 'items' });
run.progress({ current: 250, total: 500, stage: 'importing' });
});
const report = await run.flush();
if (deliveryIsDegraded(report)) console.warn('BernOps delivery degraded', report);
}
);
} finally {
const report = await client.shutdown();
if (deliveryIsDegraded(report)) console.warn('BernOps shutdown degraded', report);
}BERNOPS_EXTERNAL_JOB_ID identifies the saved BernOps job. externalRunId identifies one durable attempt of that job. If externalRunId is omitted, the generated value is replay-safe only inside the current process.
Prefer client.run() because it closes open steps, flushes telemetry, completes or fails an external run, and preserves the callback's return value or exact thrown error. Use startRun() only when an external caller must own one run across several functions; await complete() or fail() before shutdown.
Native Playwright quick start
Do not add a tarball or package installation command to a saved BernOps Playwright job. With the approved private bootstrap installed, run bernops setup and choose 3 (automation worker) or 4 (both); the menu defaults to auth-only. Unified worker setup obtains a signed immutable release with the exact private SDK already bundled, checks real SDK/browser readiness, and requires separate workspace approval for native_bridge_v1 plus a confirmed worker connection. No source checkout build, public SDK install, or artifact mount is part of that flow. A local package installation is useful for authoring and type-checking only; it does not provision the deployed worker. Existing legacy workers are not silently upgraded. Ask the operator to check release availability and approved capabilities if the package import or native capability is unavailable.
The BernOps worker injects the bridge configuration for each execution. Do not set or copy AB_LOG_URL, BERNOPS_BRIDGE_TOKEN, BERNOPS_RUN_ID, or BERNOPS_PROJECT_ID, and do not add a BERNOPS_API_KEY to the job.
import { test, expect } from '@playwright/test';
import { BernOps } from '@bernops/automation';
test('checkout', async ({ page }) => {
const client = BernOps.fromEnv({
transport: 'native',
onDiagnostic(diagnostic) {
console.warn(`[bernops:${diagnostic.code}] ${diagnostic.message}`);
},
});
try {
const capabilities = await client.capabilities();
expect(capabilities.lifecycleOwner).toBe('worker');
await client.run({ name: 'checkout' }, async (run) => {
run.log.info('opening checkout');
await run.step('submit order', async () => {
await page.goto('/checkout');
});
});
} finally {
await client.shutdown();
}
});The worker and Playwright own start, heartbeat, and terminal status. run.complete() and run.fail() exist on the shared type but reject in native mode with BernOpsCapabilityError and code === 'lifecycle_owned_by_worker'. Call capabilities() before optional behavior: native persistent attachments are available only when persistentAttachments is true, and the returned size limits are authoritative.
Telemetry, progress, and attachments
| API | Behavior |
|---|---|
run.log.debug/info/warn/error | Captures a structured log and returns queue admission immediately. |
run.step / run.startStep | Creates callback-managed or manually ended steps. Pass a real parent step handle for detached work. |
run.metric.gauge/count/rate/duration | Captures low-cardinality metrics. Durations are milliseconds. |
run.progress | Reports count, percent, or indeterminate advisory progress. |
run.attach | Hashes, uploads, and persistently acknowledges one replayable attachment. Always await it. |
run.flush | Waits for accepted work through the current watermark and leaves the run writable. |
Progress may coalesce while pending, so sequence gaps are valid. It never determines pass/fail and BernOps does not fabricate 100 percent for a running execution. Keep metric names and tags low-cardinality: do not use email addresses, user IDs, request IDs, timestamps, full URLs, or free-form values as names or tags.
An attachment must use exactly one replayable source: path, Uint8Array or Blob data, or an open factory that returns a fresh unread stream for every delivery attempt.
// `run` is the handle supplied to a client.run() callback.
const attachment = await run.attach({
path: '/tmp/import-summary.json',
fileName: 'import-summary.json',
contentType: 'application/json',
});
console.log(attachment.attachmentId, attachment.sizeBytes, attachment.duplicate);Supported content types are PNG, JPEG, WebP, plain text, JSON, CSV, and PDF. Do not change path or stream bytes until attach() settles. The acknowledgement represents a persistent metadata and storage commit, not queue admission.
Delivery, errors, and security
Log, metric, and progress methods return an EnqueueResult; accepted: true means only that the in-memory queue accepted the event. Network delivery is observed at flush(), external terminal methods, and shutdown() through a DeliveryReport containing delivered, duplicate, dropped, pending, failed, timedOut, and droppedByReason.
The default failurePolicy: 'diagnostic' preserves the automation result when telemetry degrades and reports the problem through onDiagnostic and delivery reports. Choose failurePolicy: 'strict' only when successful business work should fail because required telemetry could not be delivered. Under strict policy, explicit delivery boundaries can throw BernOpsDeliveryError; the SDK still never replaces an error originally thrown by your callback.
Branch on public error classes, code, retryable, and class-specific safe fields rather than matching message text:
| Error | Typical action |
|---|---|
BernOpsConfigurationError | Fix incomplete or ambiguous transport, URL, credential, delivery, or redaction configuration. |
BernOpsAuthenticationError | Rotate or correct the key, or investigate the current worker assignment. Do not retry unchanged credentials. |
BernOpsValidationError | Correct unsupported or invalid telemetry, progress, metric, or attachment input. |
BernOpsCapabilityError | Branch on negotiated capabilities and lifecycle ownership. |
BernOpsStateError | Stop conflicting terminal or step calls and check for stale state. |
BernOpsTransportError | Inspect status, requestId, retryAfterMs, and retryable. |
BernOpsDeliveryError | Inspect the immutable report returned by a strict delivery boundary. |
BernOpsFinalizedRunError | Treat the durable attempt as already terminal or intentionally use a new business-attempt ID. |
- External endpoints require HTTPS. Plain HTTP is accepted only for literal
127.0.0.1or[::1]test URLs configured explicitly withallowInsecureLoopback: true; URL credentials and redirects are rejected. - Redaction is defense in depth. Never capture authorization headers, cookies, tokens, personal data, card data, or unrestricted request and response bodies.
- Attachment bytes are not redacted. Scrub files before upload, avoid unnecessary screenshots, and apply suitable retention and access controls.
- Queues and retries are bounded and in memory; they do not survive
SIGKILL, OOM, host loss, or power loss. - Importing the package installs no process signal handlers. The host owns signal policy, but every normal path must await a bounded
client.shutdown()infinally.
Troubleshooting
| Symptom | Check |
|---|---|
| Transport is incomplete or ambiguous | Select external or native explicitly and remove variables belonging to the other transport. |
| External URL is rejected | Use HTTPS. For a local test, use an explicit constructor with literal loopback and allowInsecureLoopback: true. |
| Authentication fails | Verify job/project binding and key rotation. Do not retry an unchanged rejected credential. |
| Native identity or capability fails | Create a new client inside the current worker assignment; never reuse copied bridge values. |
| A durable external run is finalized | Reuse it only to observe the prior terminal result, or choose a new ID for a genuinely new business attempt. |
| Delivery is pending, dropped, failed, or timed out | Inspect onDiagnostic, the report, and droppedByReason; fix transport and timeout causes before increasing queue bounds. |
| Attachment changes or cannot be reopened | Keep path/byte sources stable and return a new identical unread stream from each factory call. |
| The process does not exit | Await attachments, flush or terminal boundaries, and client.shutdown(); then clean up host-owned timers and listeners. |
Do not hide permanent failures with unbounded retries, broader scopes, disabled TLS, redirect following, raw response-body logging, or larger queues.
Related references
The installed artifact includes its matching README.md, CHANGELOG.md, COMPATIBILITY.md, UPGRADING.md, and TypeScript declarations. Use those versioned files and your IDE's package-root types for exact signatures; do not copy an API signature from a different SDK revision.