Docs / Automation Plugins / Plugin Quickstart

Plugin Quickstart

Build and test the smallest useful automation plugin.

Developer Preview: This quickstart shows the planned authoring flow. Package names and test helpers may change before the runtime ships.

What you will build

A one-file plugin that:

  • declares one provider
  • runs setup
  • waits for one email OTP
  • exposes one read-only skill through the authenticated browser session
  • runs in the local test harness

Install

npm init -y
npm install @atomic/plugin-preview zod
npm install -D typescript vitest

Use this package.json shape:

{
  "name": "atomic-plugin-sample-market",
  "version": "0.1.0",
  "type": "module",
  "private": true,
  "scripts": {
    "test": "vitest run"
  }
}

Plugin in 100 lines

Create src/plugin.ts.

import type { SetupHandler, SkillHandler } from '@atomic/plugin-preview';

export const manifest = {
  id: 'sample-market',
  name: 'Sample Market',
  version: '0.1.0',
  runtimeRange: '^0.1',
  capabilities: {
    browser: true,
    otp: { email: true },
    state: true,
    logs: true,
    http: { egressDomains: ['api.sample.example'] },
  },
  email: {
    allowedSenderDomains: ['sample.example', 'mail.sample.example'],
  },
  setup: { required: true },
  skills: [
    {
      name: 'check_order',
      description: 'Check an order status.',
      mutating: false,
      timeoutMs: 30_000,
      maxConcurrency: 4,
      inputSchema: {
        type: 'object',
        properties: {
          orderRef: { type: 'string' },
        },
        required: ['orderRef'],
        additionalProperties: false,
      },
    },
  ],
} as const;

export const setup: SetupHandler = async ({ agent, browser, otp, state, log }) => {
  await browser.navigate('https://sample.example/signup');
  await browser.act(`Create an account for ${agent.email}.`);

  const code = await otp.waitForEmailCode({
    service: 'sample-market',
    timeoutMs: 120_000,
  });

  await browser.act(`Enter verification code ${code}.`);
  await state.merge('setup', {
    stage: 'active',
    updatedAt: new Date().toISOString(),
  });

  log.info('Prepared Sample Market account');
  return { status: 'active' };
};

type CheckOrderInput = {
  orderRef: string;
};

export const checkOrder: SkillHandler<CheckOrderInput> = async ({
  input,
  browser,
  state,
  log,
}) => {
  const setupState = await state.get<{ stage?: string }>('setup');
  if (setupState?.stage !== 'active') {
    return {
      ok: false,
      errorCode: 'setup_required',
      message: 'Setup has not completed for this provider.',
      retryable: false,
    };
  }

  const response = await browser.sessionFetch('https://api.sample.example/orders/status', {
    method: 'POST',
    json: { orderRef: input.orderRef },
  });

  const status = await response.json();
  log.info('Checked order status', { orderRef: input.orderRef });

  return { ok: true, data: { status } };
};

Test it

Create test/plugin.test.ts.

import { describe, expect, it } from 'vitest';
import { createPluginTestHarness } from '@atomic/plugin-preview/testing';
import { checkOrder, manifest, setup } from '../src/plugin';

describe('sample-market plugin', () => {
  it('runs setup and a read-only skill', async () => {
    const harness = createPluginTestHarness({
      manifest,
      handlers: {
        setup,
        skills: { check_order: checkOrder },
      },
      agent: {
        email: 'agent@demo.atomic.bond',
        phone: '+15555550100',
      },
      browser: {
        actions: [
          { ok: true, summary: 'Account form submitted' },
          { ok: true, summary: 'Verification code submitted' },
        ],
      },
      otp: {
        email: {
          'sample-market': ['123456'],
        },
      },
      http: {
        'https://api.sample.example/orders/status': {
          status: 200,
          json: { status: 'delivered' },
        },
      },
    });

    await harness.validateManifest(manifest);
    await expect(harness.runSetup({})).resolves.toMatchObject({
      status: 'active',
    });
    await expect(
      harness.runSkill('check_order', { orderRef: 'SM-100' })
    ).resolves.toMatchObject({
      ok: true,
      data: { status: { status: 'delivered' } },
    });
  });
});

Run:

npm test

Read Build a Plugin when this passes. Use Reference when you need exact types.