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 zodPeer 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 tosrc/swirls.gen.tsif omitted.
Initialize config from scratch with:
swirls configure2. Generate types and form registry
From your project root (with config and auth in place):
swirls auth login # once
swirls form genThis generates src/swirls.gen.ts (or your genPath) with:
- Zod schemas for each form in the project
- A
registrymapping form names tonameandschema 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.
Related
- Schemas: How schemas power validation and code generation.
- Triggers (Forms): Configure form triggers on root nodes.
- TypeScript generation: CLI code generation details.