SDK

Code generation

Integrate Swirls into your application with generated types, form adapters, and the SDK.

Configure a project, generate types from its form schemas, and use the SDK to submit those forms from a React app. The final sections cover submissions from a server or script.

Installation

bun add @swirls/sdk react zod
# or
npm install @swirls/sdk react zod

Peer dependencies: react (18+), zod. The CLI is used for code generation. See CLI for installation.

1. Configuration

Create a Swirls config file so the CLI knows your project and where to write generated code.

swirls.config.ts (in your project root):

import { defineConfig } from '@swirls/sdk/config'

export default defineConfig({
  projectId: 'your-project-uuid',
  genPath: 'src/swirls.gen.ts',
})
  • projectId: The Swirls project that owns your forms (and workflows). Get it from the dashboard or API.
  • genPath: Path for the generated file (Zod schemas, form registry, registerForms()). Defaults to src/swirls.gen.ts if omitted.

Initialize config from scratch with:

swirls configure

2. Generate types and form registry

From your project root (with config and auth in place):

swirls auth login   # once
swirls form gen

This generates src/swirls.gen.ts (or your genPath) with:

  • Zod schemas for each form in the project
  • A registry mapping form names to name and schema
  • registerForms(): call this once at app startup
  • Module augmentation so TypeScript knows your form names when using the SDK

Example generated file (conceptually):

// swirls.gen.ts (generated — do not edit by hand)
import { registerForm } from '@swirls/sdk/form'
import { z } from 'zod'

// Form schema: contact_form
const contactFormSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  message: z.string().optional(),
})
type ContactFormSchema = z.infer<typeof contactFormSchema>

export const registry = {
  'contact_form': {
    name: 'contact_form',
    schema: contactFormSchema,
  },
} as const

export function registerForms() {
  registerForm('contact_form', registry['contact_form'])
}

declare module '@swirls/sdk/form' {
  interface FormRegistry {
    'contact_form': {
      name: 'contact_form'
      schema: typeof contactFormSchema
    }
  }
}

After adding or changing forms in your .swirls files, deploy and run swirls form gen again so your app stays in sync.

3. Register forms at startup

Call registerForms() once when your app boots so the SDK can resolve form names to schemas.

Next.js App Router: register forms in a module imported by your client form components. Registering only in a server layout does not populate the browser's registry.

// app/form-setup.ts
'use client'

import { registerForms } from '@/swirls.gen'
registerForms()

Import ./form-setup from each client entry that uses the adapter before rendering it. Mark interactive form components with 'use client'.

Vite / CRA / single entry: in your entry file:

// main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import { registerForms } from './swirls.gen'

registerForms()

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

If you skip this step, useSwirlsFormAdapter will throw: Form "..." is not registered. Did you call registerForms()?

4. Using the form adapter (headless)

The form must be enabled and have visibility: public. Pass { projectId: "<project-id>" } as the adapter's third argument to identify its project. The adapter defaults to Cloud.

useSwirlsFormAdapter(formName, defaultValues) returns a type-safe adapter: submit, schema, and defaultValues. Wire it to any form library.

With TanStack Form

import { useForm } from '@tanstack/react-form'
import { useSwirlsFormAdapter } from '@swirls/sdk/form'

export function ContactForm() {
  const adapter = useSwirlsFormAdapter('contact_form', {
    name: '',
    email: '',
    message: '',
  })

  const form = useForm({
    defaultValues: adapter.defaultValues,
    validators: {
      onChange: adapter.schema.safeParse,
    },
    onSubmit: async ({ value }) => {
      await adapter.submit(value)
      // optional: use response (e.g. executionIds) for redirect or toast
    },
  })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        form.handleSubmit()
      }}
    >
      <input
        value={form.state.values.name}
        onChange={(e) => form.setFieldValue('name', e.target.value)}
        placeholder="Name"
      />
      <input
        type="email"
        value={form.state.values.email}
        onChange={(e) => form.setFieldValue('email', e.target.value)}
        placeholder="Email"
      />
      <textarea
        value={form.state.values.message}
        onChange={(e) => form.setFieldValue('message', e.target.value)}
        placeholder="Message"
      />
      <button type="submit" disabled={form.state.isSubmitting}>
        Submit
      </button>
    </form>
  )
}

With React Hook Form

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useSwirlsFormAdapter } from '@swirls/sdk/form'

export function ContactForm() {
  const adapter = useSwirlsFormAdapter('contact_form', {
    name: '',
    email: '',
    message: '',
  })

  const { register, handleSubmit, formState } = useForm({
    defaultValues: adapter.defaultValues,
    resolver: zodResolver(adapter.schema),
  })

  const onSubmit = async (data: typeof adapter.defaultValues) => {
    await adapter.submit(data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} placeholder="Name" />
      <input type="email" {...register('email')} placeholder="Email" />
      <textarea {...register('message')} placeholder="Message" />
      <button type="submit" disabled={formState.isSubmitting}>
        Submit
      </button>
    </form>
  )
}

Plain React state

import { useState } from 'react'
import { useSwirlsFormAdapter } from '@swirls/sdk/form'

export function ContactForm() {
  const adapter = useSwirlsFormAdapter('contact_form', {
    name: '',
    email: '',
    message: '',
  })

  const [values, setValues] = useState(adapter.defaultValues)
  const [submitting, setSubmitting] = useState(false)

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    const parsed = adapter.schema.safeParse(values)
    if (!parsed.success) {
      console.error(parsed.error.flatten())
      return
    }
    setSubmitting(true)
    try {
      await adapter.submit(parsed.data)
      setValues(adapter.defaultValues)
    } finally {
      setSubmitting(false)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={values.name}
        onChange={(e) => setValues((v) => ({ ...v, name: e.target.value }))}
        placeholder="Name"
      />
      <input
        type="email"
        value={values.email}
        onChange={(e) => setValues((v) => ({ ...v, email: e.target.value }))}
        placeholder="Email"
      />
      <textarea
        value={values.message}
        onChange={(e) => setValues((v) => ({ ...v, message: e.target.value }))}
        placeholder="Message"
      />
      <button type="submit" disabled={submitting}>
        Submit
      </button>
    </form>
  )
}

5. Handling submission response

adapter.submit(value) returns a Promise that resolves when the API accepts the submission. The response typically includes execution identifiers you can use for redirects or polling.

const result = await adapter.submit(value)
// result may include executionIds, message, etc. — shape depends on your API
if (result.executionIds?.length) {
  router.push(`/status/${result.executionIds[0]}`)
}

Handle errors in your UI (e.g. toast or inline message):

try {
  await adapter.submit(value)
} catch (err) {
  setError(err instanceof Error ? err.message : 'Submission failed')
}

6. Running workflows from your backend or scripts

To start a workflow from a server or script, use the authenticated workflows.executeWorkflow SDK method, or submit to a public form or webhook. Public JSON form submissions do not use an RPC bearer token; webhooks verify the optional shared-secret header declared in their definition. The payload must match the ingress schema.

Submitting a form programmatically (https://swirls.ai/api/forms/:formName; pass ?projectId= when more than one of your projects defines a form with the same name):

const response = await fetch(`https://swirls.ai/api/forms/${formName}`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    topic: 'Product launch',
    audience: 'Developers',
  }),
})
const { message, executionIds } = await response.json()

Sending a webhook payload (https://swirls.ai/triggers/webhooks/:projectId/:webhookName; include the shared-secret header when your webhook declares one):

const response = await fetch(
  `https://swirls.ai/triggers/webhooks/${projectId}/${webhookName}`,
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      event: 'order.created',
      data: { orderId: '12345' },
    }),
  },
)
const { message, executionIds } = await response.json()

Both endpoints return { message, executionIds } on success. See the API reference for the full set of endpoints.

7. API base URL

Form submissions from the adapter go to Swirls Cloud (https://swirls.ai/api) by default. There is nothing to configure for the common case.

On this page