LIVE DEMO
THIS FORM IS A .swirls FILE.
The feedback form below is backed by a real .swirls file. Submit feedback to see the same pipeline you use in the editor: normalize, AI analysis, and structured output.
feedback.swirls/file to power the form
form feedback {
label: "Product feedback"
description: "Share feedback and we analyze it with a workflow graph."
enabled: true
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
}
}
}
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: @json {
{
"type": "object",
"required": ["name", "email", "feedback", "rating"],
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"feedback": { "type": "string" },
"rating": { "type": "integer" }
},
"additionalProperties": false
}
}
outputSchema: @json {
{
"type": "object",
"required": ["name", "email", "feedback", "rating"],
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"feedback": { "type": "string" },
"rating": { "type": "integer" }
},
"additionalProperties": false
}
}
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
Submit feedback—it runs through the workflow on the left.
HOW IT WORKS
1. Define
Write your form schema, processing graph, and triggers in a single .swirls file. Version it in Git like any other code.
2. Submit
Users submit feedback. Input is validated against your JSON Schema and sent to the Swirls runtime.
3. Execute
The trigger runs your graph: normalize, analyze with an LLM, and produce structured output. Each node runs in order.