Resources
Define forms, webhooks, and schedules as workflow entry points.
What it is. The ways the outside world hands input to your system: forms collect it from people, webhooks accept it from programs, and schedules generate it on a clock.
Use it when you have forms today (Google Forms, Typeform, your website), apps that can call a webhook, or a procedure you run on a schedule.
Works with triggers (the binding that starts the workflow) and credential profiles (type: basic for public form gates).
Resources are the entry points for your workflows. Each resource type generates a trigger that starts a workflow run. Connect resources to workflows using triggers.
Forms
A form defines an input interface with a JSON Schema for its payload. Swirls server-renders the form from that schema (each property's "title" becomes the field label) and hosts it at a public endpoint; submissions are CSRF-protected. You can inline the schema with @json { … } or define a reusable top-level schema block and set schema: my_schema_name on the form (and on workflow nodes) so one definition is shared across triggers and steps.
| Field | Type | Required | Description |
|---|---|---|---|
label | string | Yes | Display name |
description | string | No | Description shown in the Portal |
visibility | string | No | public or internal. Defaults to internal. |
enabled | boolean | No | Whether the form is active |
auth | identifier | No | Name of a top-level auth block with type: basic for HTTP Basic auth gating |
schema | json block or name | No | JSON Schema for the form payload, or a reference to a schema resource |
Visibility. public forms are served at /triggers/forms/:projectId/:formName. internal forms return 404 from the hosted endpoint but remain usable from the Cloud dashboard, where submitting still fires the trigger. The default is internal.
HTTP Basic auth. Set auth: <block_name> where the referenced auth block is a credential profile with type: basic. A valid Authorization: Basic header is then required before the hosted form renders or accepts a submission. This only applies to public forms: an internal form 404s on the hosted endpoint before any auth check, so auth: on it has no effect.
schema contact_payload {
label: "Contact payload"
schema: @json {
{
"type": "object",
"required": ["name", "email", "message"],
"properties": {
"name": { "type": "string", "title": "Name" },
"email": { "type": "string", "title": "Email" },
"message": { "type": "string", "title": "Message" }
},
"additionalProperties": false
}
}
}
form contact_form {
label: "Contact Form"
description: "Collect contact information"
visibility: public
enabled: true
schema: contact_payload
}A password-protected public form:
secret portal_auth {
vars: [PORTAL_USER, PORTAL_PASS]
}
auth portal_basic {
type: basic
secrets: portal_auth
username: PORTAL_USER
password: PORTAL_PASS
}
form partner_intake {
label: "Partner Intake"
visibility: public
auth: portal_basic
enabled: true
schema: contact_payload
}Webhooks
A webhook defines a hosted HTTP POST endpoint at /triggers/webhooks/:projectId/:webhookName.
| Field | Type | Required | Description |
|---|---|---|---|
label | string | Yes | Display name |
description | string | No | Description |
enabled | boolean | No | Whether the webhook is active |
schema | json block or name | No | JSON Schema for the expected payload |
secret | <block>.<var> | No | Reference to a secret var for shared-secret verification |
header | string | No | Header name that carries the shared secret (e.g. "X-Webhook-Secret") |
Shared-secret auth. The sender puts the secret value in the configured header; Swirls compares it against your stored secret in constant time. A missing or wrong secret gets a uniform 401. Rules:
secretandheadergo together: setting one without the other is a validator error.- Without both, the webhook accepts any POST. That is allowed, but the validator warns so the choice is auditable.
- The header name must be a custom header. Standard headers that proxies or clients control (
Cookie,Host,Content-Type,X-Forwarded-*,User-Agent, and similar) are rejected;Authorizationis allowed.
Payload handling. An empty body is treated as {}. Invalid JSON gets 400. When a schema is declared, the payload is validated against it; a mismatch gets 422 with field-level errors. An accepted request gets 202 with the started execution ids. A disabled webhook returns 200 with an explanatory body so senders do not retry forever.
secret partner_creds {
vars: [PARTNER_WEBHOOK_SECRET]
}
webhook partner_events {
label: "Partner Events"
enabled: true
secret: partner_creds.PARTNER_WEBHOOK_SECRET
header: "X-Webhook-Secret"
schema: @json {
{
"type": "object",
"required": ["type", "data"],
"properties": {
"type": { "type": "string" },
"data": { "type": "object" }
},
"additionalProperties": false
}
}
}Schedules
A schedule triggers a workflow on a cron expression.
| Field | Type | Required | Description |
|---|---|---|---|
label | string | Yes | Display name |
cron | string | Yes | Standard 5-field cron expression |
timezone | string | No | IANA timezone (e.g. "America/New_York") |
enabled | boolean | No | Whether the schedule is active |
Semantics:
- Schedules fire with one-minute granularity. The
cronexpression is evaluated in the schedule'stimezone. - The payload is always
{}. A scheduled workflow's root should not expect input fields. - A schedule never overlaps itself: a run that is still going blocks the next occurrence, and the next run time is computed before firing, so a slow run does not delay the occurrence after it.
schedule daily_report {
label: "Daily Report"
cron: "0 9 * * *"
timezone: "America/New_York"
enabled: true
}Connecting resources to workflows
Resources do not run workflows on their own. You connect a resource to a workflow using a trigger. See Triggers for details.