Code generation
Integrate Swirls into your application with generated types, form adapters, and the SDK.
This guide covers the full integration workflow: configuring your project, generating TypeScript types from form schemas, embedding forms in your React app, and running graphs programmatically. All examples are ready to adapt to your stack.
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 graphs). Get it from the Portal 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 dev init2. 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 the Portal, 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 ids and schemas.
Next.js App Router — in your root layout:
// app/layout.tsx
import { registerForms } from '@/swirls.gen'
export default function RootLayout({ children }: { children: React.ReactNode }) {
registerForms()
return (
<html lang="en">
<body>{children}</body>
</html>
)
}Vite / CRA / single entry — in your entry file:
// main.tsx
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)
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 graphs from your backend or scripts
To run a graph from a server or script (for example, a cron job or webhook handler), call the Swirls API directly. Authenticate with a bearer token, then POST to the form or webhook endpoint. The payload must match the trigger's schema.
Submitting a form programmatically:
const response = await fetch(`${API_URL}/forms/${formId}`, {
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:
const response = await fetch(`${API_URL}/webhooks/${webhookId}`, {
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. Environment and API base URL
The SDK and generated code rely on an API base URL for form submissions. Set the SWIRLS_API_URL environment variable (or the equivalent in your app config) so that local development and production point at the correct Swirls API instance.
Summary
- Config —
defineConfig({ projectId, genPath })inswirls.config.ts. - Generate —
swirls form gento produce Zod schemas, registry, andregisterForms(). - Register — Call
registerForms()once at app startup. - Forms — Use
useSwirlsFormAdapter(name, defaultValues)with generated form names for type-safe, validated submission. - Backend / scripts — Use the form or webhook HTTP endpoints to run graphs with a payload that matches the trigger's schema.
Related
- Schemas — How schemas power validation and code generation.
- Triggers (Forms) — Configure form triggers on root nodes.
- TypeScript generation — CLI code generation details.