Developer section: these pages cover the S6S workflow engine for self-hosted installs and developers. On s6s.ai, API keys and MCP access are issued by S6S on request (contact us); on a self-hosted install, an admin creates them on the API Keys page. For the AI Visibility product, start at the AI Visibility docs.
Triggers
Every workflow has exactly one trigger that determines how it starts. S6S supports 4 trigger types: webhook, schedule, manual, telegram.
Webhook
Start workflows from HTTP requests
Schedule
Run workflows on a cron schedule
Manual
Run from the app or an API call
Telegram
Run on messages from a linked Telegram chat
Webhook Triggers
Webhook triggers start a workflow when an HTTP request reaches a specific URL path. Use them for third-party services that send webhooks (Stripe, GitHub, Shopify, etc.) or for your own endpoints.
Recipe configuration
The trigger fields sit directly on the trigger object:
{
"name": "Handle New Lead",
"trigger": {
"type": "webhook",
"path": "/new-lead",
"method": "POST"
},
"steps": [
{
"ref": "record_lead",
"action": "transform",
"config": {
"template": {
"name": "{{trigger.output.body.name}}",
"email": "{{trigger.output.body.email}}"
}
}
}
],
"flow": ["record_lead"]
}| Field | Type | Default | Description |
|---|---|---|---|
| path | string | required | URL path under /api/webhooks. A leading slash is added if missing. A path can belong to only one workflow on the install; a clash returns 409 WEBHOOK_PATH_CONFLICT. |
| method | "GET" | "POST" | POST | Stored with the trigger. The endpoint accepts both GET and POST requests whatever this is set to. |
Deploying a webhook-triggered workflow does not start a run unless you send "autoRun": true in the deploy request.
Deploy response
The deploy response includes a webhookTrigger object with the invoke URL and the webhook secret:
{
"ok": true,
"workflowId": "6c33d112-7297-4f02-9092-2a22135ba56b",
"versionId": "7680dc16-9169-48ad-a08d-c474b625c39f",
"version": 1,
"hash": "2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881",
"runId": null,
"connectedProviders": [],
"webhookTrigger": {
"path": "/new-lead",
"invokeUrl": "https://s6s.ai/api/webhooks/new-lead",
"secret": "9d18d6ad834aacb3cb42cab9714e177ec8be4c60e494f7f3"
},
"monitorUrl": "https://s6s.ai/workflows/6c33d112-7297-4f02-9092-2a22135ba56b",
"refToId": {
"record_lead": "4bdbdac7-7204-40bd-ac2e-8571bc6d4f74",
"__trigger__": "76447542-e948-4916-9778-1c8ac50e5da6"
}
}The secret is 48 hexadecimal characters. It is returned only on the first deploy of the workflow; later deploys return "secret_hint": "previously_generated" instead, so store it when you first get it. On a self-hosted install, the URLs in the response are built from the S6S_BASE_URL setting.
Authentication
Every request must prove it knows the webhook secret. S6S accepts any one of these:
| Method | What to send |
|---|---|
| X-Webhook-Token | Header with the secret. Preferred. |
| ?token= | Query parameter with the secret. Query strings can end up in logs, so prefer a header. |
| X-Webhook-Signature | Header with the hex HMAC-SHA256 of the raw request body, keyed with the secret. |
| Authorization: Bearer | The secret as a bearer token. |
| X-Telegram-Bot-Api-Secret-Token | Header with the secret. Telegram sends it when you register the URL with Telegram's setWebhook and its secret_token parameter. |
Example: triggering a webhook
curl -X POST "https://s6s.ai/api/webhooks/new-lead" \
-H "X-Webhook-Token: $WEBHOOK_SECRET" \
-H "Content-Type: application/json" \
-d '{
"name": "Jane Smith",
"email": "[email protected]",
"source": "landing_page"
}'BODY='{"name":"Jane Smith","email":"[email protected]"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | sed 's/^.* //')
curl -X POST "https://s6s.ai/api/webhooks/new-lead" \
-H "X-Webhook-Signature: $SIG" \
-H "Content-Type: application/json" \
-d "$BODY"S6S answers right away, before the workflow runs. The reply carries the new run ID and the trigger data; read the step results with GET /api/v1/runs/{runId}. A webhook_response step does not change this reply.
{
"runId": "7ed60fcc-f5ac-4306-a455-24bee407186c",
"workflowId": "6c33d112-7297-4f02-9092-2a22135ba56b",
"workflowVersionId": "7680dc16-9169-48ad-a08d-c474b625c39f",
"triggerPath": "/new-lead",
"triggerOutput": {
"body": { "name": "Jane Smith", "email": "[email protected]", "source": "landing_page" },
"headers": { "content-type": "application/json" },
"query": {},
"method": "POST",
"path": "/new-lead",
"receivedAt": "2026-10-06T09:00:00.000Z"
}
}To make retries safe, send an X-Idempotency-Key (or Idempotency-Key) header. A repeat with the same key for the same workflow version returns the earlier run with "idempotent": true instead of starting a new one.
Accessing webhook data
Use expressions to access the incoming request data in your workflow steps:
| Expression | Description |
|---|---|
| {{trigger.output.body}} | The body: parsed JSON when the Content-Type is application/json, otherwise the raw text. It is null when the body is empty or the JSON is invalid. |
| {{trigger.output.body.name}} | A specific field from a JSON body |
| {{trigger.output.headers}} | Request headers, without Cookie, Authorization, Proxy-Authorization, X-Webhook-Token, X-Webhook-Signature and X-Telegram-Bot-Api-Secret-Token |
| {{trigger.output.query}} | Query string parameters, without token |
| {{trigger.output.method}} | HTTP method (GET or POST) |
| {{trigger.output.path}} | The trigger path |
| {{trigger.output.receivedAt}} | ISO timestamp when the request arrived |
Input validation
A workflow deployed in the raw IR format can carry an inputSchema (a JSON Schema). When it does, a webhook request whose body doesn't match is rejected with 422 and no run starts. A Recipe can't declare an inputSchema, and POST /api/v1/workflows/{id}/run does not check it.
{
"error": "INPUT_VALIDATION_FAILED",
"details": ["/: must have required property 'email'"]
}Error responses
| Status | error | When |
|---|---|---|
| 400 | webhook_path_not_found_or_disabled | No enabled workflow uses this path. |
| 401 | WEBHOOK_TOKEN_INVALID | The secret is missing or wrong. |
| 402 | INSUFFICIENT_CREDITS | The workflow owner doesn't have enough credits. The body includes balance and estimatedCost. |
| 403 | WEBHOOK_SECRET_NOT_CONFIGURED | The workflow has no secret. Deploy it again with the deploy API to create one. |
| 422 | INPUT_VALIDATION_FAILED | The body doesn't match the workflow's inputSchema. |
| 429 | RATE_LIMIT_EXCEEDED | More than 60 requests in a minute from one IP address to one path (the default; self-hosted installs can change it with S6S_WEBHOOK_RATE_LIMIT). The Retry-After header says when to try again. |
Schedule Triggers
Schedule triggers run a workflow automatically on a recurring basis using standard cron expressions. Use these for periodic data syncs, report generation, monitoring, cleanup jobs, and more.
Recipe configuration
{
"name": "Daily Report",
"trigger": {
"type": "schedule",
"cron": "0 9 * * *",
"timezone": "America/New_York"
},
"steps": [
{
"ref": "build_report",
"action": "transform",
"config": {
"template": { "generated_at": "{{trigger.output.fired_at}}" }
}
}
],
"flow": ["build_report"]
}| Field | Type | Default | Description |
|---|---|---|---|
| cron | string | required | Five-field cron expression: minute hour day-of-month month day-of-week |
| timezone | string | UTC | IANA timezone name (e.g. America/New_York, Europe/London) |
Deploying a schedule-triggered workflow saves the schedule; it never starts a run on deploy.
Common cron patterns
| Pattern | Description |
|---|---|
| */5 * * * * | Every 5 minutes |
| 0 * * * * | Every hour (on the hour) |
| 0 9 * * * | Daily at 9:00 AM |
| 0 9 * * 1-5 | Weekdays at 9:00 AM |
| 0 0 * * * | Daily at midnight |
| 0 0 1 * * | First day of every month at midnight |
| 0 */4 * * * | Every 4 hours |
| 30 8 * * 1 | Every Monday at 8:30 AM |
Cron expressions use 5 fields: minute hour day-of-month month day-of-week. Each field takes *, a number, a range (1-5), a list (1,15) or a step (*/10, 0-30/10). Day-of-week runs from 0 (Sunday) to 6.
Timezone handling
Schedules use IANA timezone names. If no timezone is specified, the schedule runs in UTC. Common timezones:
UTCAmerica/New_YorkAmerica/ChicagoAmerica/DenverAmerica/Los_AngelesEurope/LondonEurope/BerlinAsia/TokyoAsia/ShanghaiAustralia/SydneyAccessing schedule data
The trigger output says when the scheduled run started:
| Expression | Description |
|---|---|
| {{trigger.output.fired_at}} | ISO timestamp set when the scheduled run starts |
| {{trigger.output.scheduledAt}} | The same timestamp under a second name |
When schedules run
S6S checks schedules once a minute and starts at most one run per workflow per minute. A due run is skipped when the workflow still has a run in progress, or when its owner doesn't have enough credits.
Manual Triggers
Manual triggers start a workflow on demand — from the app or via the API. This is the simplest trigger type and requires no configuration. A manual-triggered workflow also runs once right after deploy, unless the deploy request sends "autoRun": false.
Recipe configuration
"trigger": { "type": "manual" }How to trigger
From the app
Open the workflow at /workflows/{id} and click "Run Now". These runs start with no input.
Via the API
Send a POST request to the run endpoint with an API key and optional input data.
Triggering via API
curl -X POST https://s6s.ai/api/v1/workflows/6c33d112-7297-4f02-9092-2a22135ba56b/run \
-H "Authorization: Bearer s6s_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"input": {
"name": "test",
"count": 42,
"items": ["a", "b", "c"]
}
}'The endpoint takes an API key only. The input field is optional; it becomes {{trigger.output.payload}} in your workflow steps. A body without an input key is used as the input itself. The workflow must be enabled (deploying enables it). The call returns at once:
{
"runId": "7ed60fcc-f5ac-4306-a455-24bee407186c",
"workflowId": "6c33d112-7297-4f02-9092-2a22135ba56b",
"versionId": "7680dc16-9169-48ad-a08d-c474b625c39f",
"version": 1,
"status": "running"
}Errors: 400 for a malformed ID, a disabled workflow (WORKFLOW_DISABLED) or one with no deployed version (NO_VERSION); 404 when the workflow isn't found; 402 when credits are insufficient.
Accessing trigger data
| Expression | Description |
|---|---|
| {{trigger.output.payload}} | The input object from the run request ({} if none) |
| {{trigger.output.payload.name}} | A specific field from the input |
| {{trigger.output.triggeredAt}} | ISO timestamp set when the run's trigger step executes |
| {{trigger.output.triggeredBy}} | Always "api" |
Full example
{
"name": "Greet User",
"trigger": { "type": "manual" },
"steps": [
{
"ref": "greet",
"action": "transform",
"config": {
"template": {
"message": "Hello, {{trigger.output.payload.name}}!",
"time": "{{trigger.output.triggeredAt}}"
}
}
}
],
"flow": ["greet"]
}curl -X POST https://s6s.ai/api/v1/workflows/6c33d112-7297-4f02-9092-2a22135ba56b/run \
-H "Authorization: Bearer s6s_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "input": { "name": "Ahmad" } }'
curl https://s6s.ai/api/v1/runs/7ed60fcc-f5ac-4306-a455-24bee407186c \
-H "Authorization: Bearer s6s_your_api_key"The first call returns the run ID. In the second response, the greet step's entry in steps has the output { "message": "Hello, Ahmad!", "time": "2026-10-06T09:00:00.000Z" }. Steps are identified by nodeId; the deploy response's refToId maps each ref to its node ID.
Telegram Triggers
Telegram triggers start a workflow when a message arrives from a Telegram chat linked to your S6S account. They need a Telegram bot connected to the S6S install.
Recipe configuration
{
"name": "Echo Telegram Message",
"trigger": { "type": "telegram" },
"steps": [
{
"ref": "reply",
"action": "telegram",
"config": {
"action": "send_message",
"chatId": "{{trigger.output.chatId}}",
"text": "You said: {{trigger.output.text}}"
},
"credential": "default"
}
],
"flow": ["reply"]
}| Field | Type | Default | Description |
|---|---|---|---|
| credential | string | — | Optional label stored on the trigger. Message routing does not depend on it. |
How it starts
- Link a chat: send
/startto the S6S bot and open the link it replies with. - After that, each text message from the linked chat starts a run of every enabled workflow you own that has a telegram trigger. Messages without text don't start runs.
- The bot's own commands (
/start,/help,/list,/status,/run,/new,/unlink) are handled by the bot and don't start runs. Other text, including unknown commands, does. - Deploying a telegram-triggered workflow starts no run (there is no message yet) unless the deploy request sends
"autoRun": true. - To use your own Telegram bot instead, register the deploy response's
telegramTrigger.invokeUrlwith Telegram's setWebhook and passtelegramTrigger.secretas its secret_token. The secret is shown on the first deploy only.
Accessing message data
| Expression | Description |
|---|---|
| {{trigger.output.text}} | The message text |
| {{trigger.output.chatId}} | The chat ID, as a string |
| {{trigger.output.from}} | The sender object from Telegram (id, username, first_name, ...) |
| {{trigger.output.chat}} | The chat object from Telegram |
| {{trigger.output.messageId}} | Telegram message ID |
| {{trigger.output.date}} | Message time as a Unix timestamp (seconds) |
| {{trigger.output.receivedAt}} | ISO timestamp when S6S received the message |
Trigger Comparison
| Feature | Webhook | Schedule | Manual | Telegram |
|---|---|---|---|---|
| Starts when | HTTP request received | Cron schedule is due | Run Now in the app or API call | Message from a linked chat |
| Config needed | path (method optional) | cron (timezone optional) | None | None |
| Runs on deploy | Only with autoRun: true | Never | Yes, unless autoRun: false | Yes, unless autoRun: false |
| Trigger data | body, headers, query, method, path, receivedAt | fired_at, scheduledAt | payload, triggeredAt, triggeredBy | text, chatId, from, chat, messageId, date, receivedAt |
| Use case | External integrations, APIs | Periodic jobs, reports | Testing, ad-hoc runs | Chat-driven automations |
Ready to trigger your first workflow?
The quick start walks through deploying and running a workflow with the API.