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 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:

Recipe with a webhook trigger
{
  "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"]
}
FieldTypeDefaultDescription
pathstringrequiredURL 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"POSTStored 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:

Deploy response
{
  "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:

MethodWhat to send
X-Webhook-TokenHeader with the secret. Preferred.
?token=Query parameter with the secret. Query strings can end up in logs, so prefer a header.
X-Webhook-SignatureHeader with the hex HMAC-SHA256 of the raw request body, keyed with the secret.
Authorization: BearerThe secret as a bearer token.
X-Telegram-Bot-Api-Secret-TokenHeader 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 (secret in a header)
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"
  }'
shell (signed body)
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.

Response
{
  "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:

ExpressionDescription
{{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.

422 response
{
  "error": "INPUT_VALIDATION_FAILED",
  "details": ["/: must have required property 'email'"]
}

Error responses

StatuserrorWhen
400webhook_path_not_found_or_disabledNo enabled workflow uses this path.
401WEBHOOK_TOKEN_INVALIDThe secret is missing or wrong.
402INSUFFICIENT_CREDITSThe workflow owner doesn't have enough credits. The body includes balance and estimatedCost.
403WEBHOOK_SECRET_NOT_CONFIGUREDThe workflow has no secret. Deploy it again with the deploy API to create one.
422INPUT_VALIDATION_FAILEDThe body doesn't match the workflow's inputSchema.
429RATE_LIMIT_EXCEEDEDMore 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

Recipe with a schedule trigger
{
  "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"]
}
FieldTypeDefaultDescription
cronstringrequiredFive-field cron expression: minute hour day-of-month month day-of-week
timezonestringUTCIANA 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

PatternDescription
*/5 * * * *Every 5 minutes
0 * * * *Every hour (on the hour)
0 9 * * *Daily at 9:00 AM
0 9 * * 1-5Weekdays 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 * * 1Every 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/Sydney

Accessing schedule data

The trigger output says when the scheduled run started:

ExpressionDescription
{{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

Recipe trigger config
"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

POST /api/v1/workflows/:id/run
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:

Response
{
  "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

ExpressionDescription
{{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

Manual trigger with input
{
  "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"]
}
Run it, then read the result
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

Recipe with a telegram trigger
{
  "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"]
}
FieldTypeDefaultDescription
credentialstring—Optional label stored on the trigger. Message routing does not depend on it.

How it starts

  • Link a chat: send /start to 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.invokeUrl with Telegram's setWebhook and pass telegramTrigger.secret as its secret_token. The secret is shown on the first deploy only.

Accessing message data

ExpressionDescription
{{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

FeatureWebhookScheduleManualTelegram
Starts whenHTTP request receivedCron schedule is dueRun Now in the app or API callMessage from a linked chat
Config neededpath (method optional)cron (timezone optional)NoneNone
Runs on deployOnly with autoRun: trueNeverYes, unless autoRun: falseYes, unless autoRun: false
Trigger databody, headers, query, method, path, receivedAtfired_at, scheduledAtpayload, triggeredAt, triggeredBytext, chatId, from, chat, messageId, date, receivedAt
Use caseExternal integrations, APIsPeriodic jobs, reportsTesting, ad-hoc runsChat-driven automations

Ready to trigger your first workflow?

The quick start walks through deploying and running a workflow with the API.