This form is a .swirls file.
The feedback form below is backed by real Swirls. Submit something and the same engine that runs in production handles your input: normalize, analyze with an LLM, persist structured output.
schema feedback_submission {
label: "Feedback submission"
description: "Shared payload for the feedback form and normalize step"
schema: @json {
{
"type": "object",
"required": ["name", "email", "feedback", "rating"],
"properties": {
"name": { "type": "string", "title": "Name" },
"email": { "type": "string", "title": "Email", "format": "email" },
"feedback": { "type": "string", "title": "Feedback" },
"rating": {
"type": "integer",
"title": "Rating",
"minimum": 1,
"maximum": 5
}
},
"additionalProperties": false
}
}
}
form feedback {
label: "Product feedback"
description: "Share feedback and we analyze it with a workflow graph."
enabled: true
schema: feedback_submission
}
secret slack_feedback_webhook {
label: "Slack incoming webhook"
description: "Three path segments from hooks.slack.com/services/T/B/token"
vars: [SLACK_WEBHOOK_TEAM_ID, SLACK_WEBHOOK_CHANNEL_ID, SLACK_WEBHOOK_TOKEN]
}
graph process_feedback {
label: "Process feedback"
description: "Normalize input, analyze with AI, then notify Slack"
root {
type: code
label: "Normalize"
inputSchema: feedback_submission
outputSchema: feedback_submission
code: @ts {
const { name, email, feedback, rating } = context.nodes.root.input
return {
name: name.trim(),
email: email.toLowerCase().trim(),
feedback: feedback.trim(),
rating: Math.min(5, Math.max(1, Number(rating) || 1)),
}
}
}
node analyze {
type: ai
kind: object
label: "Analyze feedback"
schema: @json {
{
"type": "object",
"required": ["sentiment", "category"],
"properties": {
"sentiment": { "type": "string" },
"category": { "type": "string" }
}
}
}
model: "google/gemini-2.5-flash"
prompt: @ts {
const { feedback, rating } = context.nodes.root.output
return `You classify product feedback. Rating is 1-5. Return JSON with:
- sentiment: one of positive, neutral, negative
- category: one of bug, feature_request, praise, other
Feedback: ${feedback}
Rating: ${rating}`
}
}
node assemble {
type: code
label: "Assemble submission"
schema: @json {
{
"type": "object",
"required": ["name", "email", "feedback", "rating", "sentiment", "category", "receivedAt"],
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"feedback": { "type": "string" },
"rating": { "type": "integer" },
"sentiment": { "type": "string" },
"category": { "type": "string" },
"receivedAt": { "type": "string" }
}
}
}
code: @ts {
const { name, email, feedback, rating } = context.nodes.root.output
const { sentiment, category } = context.nodes.analyze.output
return {
name,
email,
feedback,
rating,
sentiment,
category,
receivedAt: new Date().toISOString(),
}
}
}
node notify_slack {
type: http
label: "Notify Slack"
method: "POST"
secrets: {
slack_feedback_webhook: [SLACK_WEBHOOK_TEAM_ID, SLACK_WEBHOOK_CHANNEL_ID, SLACK_WEBHOOK_TOKEN]
}
url: @ts {
const teamId = context.secrets.slack_feedback_webhook.SLACK_WEBHOOK_TEAM_ID
const channelId = context.secrets.slack_feedback_webhook.SLACK_WEBHOOK_CHANNEL_ID
const token = context.secrets.slack_feedback_webhook.SLACK_WEBHOOK_TOKEN
return `https://hooks.slack.com/services/${teamId}/${channelId}/${token}`
}
body: @ts {
type FeedbackSlackPayload = {
name: string
email: string
feedback: string
rating: number
sentiment: string
category: string
}
const formatFeedbackSlackText = (p: FeedbackSlackPayload) => {
const meta = `From: ${p.name} (${p.email})
Rating: ${p.rating}/5 | Sentiment: ${p.sentiment} | Category: ${p.category}`
return ["New product feedback", meta, "", p.feedback].join("\n")
}
const text = formatFeedbackSlackText({
name: context.nodes.root.output.name,
email: context.nodes.root.output.email,
feedback: context.nodes.root.output.feedback,
rating: context.nodes.root.output.rating,
sentiment: context.nodes.analyze.output.sentiment,
category: context.nodes.analyze.output.category,
})
return JSON.stringify({ text: text })
}
}
flow {
root -> analyze
analyze -> assemble
assemble -> notify_slack
}
}
trigger on_feedback {
form:feedback -> process_feedback
enabled: true
}
Try the demo
Three steps from submission to result.
Define
Write the form schema, the processing graph, and the trigger across .swirls files. Version them alongside the rest of your code.
Submit
Users submit the form. The runtime validates the payload against your JSON Schema and dispatches the trigger.
Execute
The graph runs end-to-end: normalize, analyze with an LLM, and produce structured output. Every step is checkpointed.
Five minutes from install to an agent you'd ship.
Install the CLI. Write .swirls files. Run them locally with the same engine that runs in production. When you're ready, git push or swirls cloud deploy. No CI/CD to build.
Building something with Swirls? Come hang out in Discord.
Join the Discord