Triggers
The four ways a published workflow starts — API, webhook, form, and recurrence.
A published workflow can be started four ways from outside Studio. All four run the same deployed version of the graph and are subject to the same concurrency ceiling; they differ in who authenticates, and in how the payload reaches {{ trigger.* }}.
| Trigger | Authenticated by | trigger.* binding | Response |
|---|---|---|---|
| API | X-API-Key | your input object, directly | 200 or 202 |
| Webhook | HMAC signature or an API key | the request body, under body | 202 |
| Form | its own mode — none, Basic, or Elizon login | the submitted field values, under body | an acknowledgment page (202) |
| Recurrence | nothing — Elizon starts it | empty ({}) | n/a |
API
The path most integrations want. POST /api/v1/workflows/{workflowId}/run blocks for up to 30 seconds and returns the result; POST /api/v1/workflows/{workflowId}/run-async returns a run id immediately. Both are covered in full in the API reference.
The graph does not need a Manual trigger node for this to work — publishing is the only prerequisite.
Webhook
Lets a third-party system that has no Elizon API key — a payment provider, a CRM, a form backend — start a run by POSTing to a URL unique to your deployment.
POST /api/v1/workflows/{workflowId}/trigger/{webhookToken}The webhookToken is opaque and is never the workflow's own id. It is carried forward unchanged every time you republish — only a workflow that has never been published mints a new one — so a URL you have already handed to a third-party system keeps working across deploys (see Publishing). Studio shows you both the URL and its signing secret. Add an HTTP Request node to the graph so the payload has somewhere to land.
Authenticating a webhook call
Either mechanism is sufficient on its own:
HMAC signature. Send two headers:
| Header | Value |
|---|---|
X-Elizon-Timestamp | Unix seconds, as sent |
X-Elizon-Signature | HMAC-SHA256(secret, "<timestamp>.<raw body>"), hex |
The timestamp is signed inside the payload, not merely sent alongside it, so a captured request cannot be replayed with a fresh timestamp header. Requests whose timestamp is more than 5 minutes from the server's clock are rejected. Sign the exact bytes of the header and the raw body — re-serializing either will produce a signature that does not match.
import hashlib, hmac, json, time, httpx
secret = "whsec_..." # from Studio
body = json.dumps({"orderId": "A-1001"})
ts = str(int(time.time()))
sig = hmac.new(secret.encode(), f"{ts}.{body}".encode(), hashlib.sha256).hexdigest()
httpx.post(
f"https://api.elizon.com/api/v1/workflows/{workflow_id}/trigger/{token}",
headers={
"Content-Type": "application/json",
"X-Elizon-Timestamp": ts,
"X-Elizon-Signature": sig,
},
content=body,
)API key. Send X-API-Key: elz_... (or the equivalent Authorization: Bearer elz_...) and no signature is required. The key must belong to the workflow's own organization, and a project-scoped key to the workflow's project.
X-Elizon-Timestamp is required either way
Replay protection is applied to both authentication paths, so X-Elizon-Timestamp is
mandatory even when you authenticate with an API key and send no signature at all. A request
without it — or with one outside the window above — gets exactly the same uninformative 401 as a
bad signature.
Webhook responses
| Status | Meaning |
|---|---|
202 | Accepted — body is { "executionId": "..." }. The run proceeds in the background. |
400 | Authenticated, but the body is not valid JSON. |
401 | Neither the signature nor the key authenticated the request. |
404 | No active deployment matches that token — including an unpublished or deleted workflow. |
413 | Body exceeded the 1 MB limit. |
429 | Rate limit, or the concurrency ceiling. |
404 is deliberately uninformative
An unknown token, a token belonging to another organization, and a token whose workflow has been
unpublished all return the identical 404 body. That is intentional — a webhook URL is a
credential, and distinguishing those cases would let a caller probe for valid ones.
Because the body arrives under body, a webhook-driven graph reads its payload as {{ trigger.body.orderId }} — see Building a workflow.
Form
Lets a person start a run by filling in a hosted form — a request form, an intake questionnaire, an internal ticket — with nothing for you to build or host. Add a Form Trigger node, declare its fields, and publish.
https://<the origin you open Studio on>/forms/{formToken}The form page is served by the app, not by the API, so this is not an api. host — the panel builds the link from the origin you are already on.
The formToken is opaque, is never the workflow's own id, and is minted by publishing — Studio's Form Trigger panel shows the real link once a deployment exists, and says the workflow is not published yet until then. It is carried forward unchanged every time you republish, so a URL you have already handed out keeps working across deploys. A workflow whose published graph has no Form Trigger node has no token at all.
Each field carries a name and a human-readable label — deliberately two things, since ticketId is the right data key and the wrong question to put in front of a person. Eleven field types are available (text, textarea, number, email, password, date, dropdown, radio, checkboxes, file, hidden); the three choice types carry their own options, and a file field carries its own Accepted file types and Allow multiple files options — see File uploads below.
The three auth modes
Set on the node, applied by the published deployment:
| Mode | Who gets in |
|---|---|
| None | Anyone with the link. |
| Basic | A username and password you set on the node. The password is bcrypt-hashed at publish time and never stored in the graph. |
| Elizon login | A signed-in, active member of the workflow's own organization — not any Elizon user. |
Under Basic and Elizon login the field list is itself protected: an unauthenticated visitor cannot see what the form even asks, let alone submit to it. A visitor who is signed in but belongs to another organization gets a specific "not a member of this organization" state rather than a silent empty form or a second sign-in prompt.
Changing the mode or the username takes effect on the next publish — and a username change has to carry a new password with it, because a stored hash is only kept for the username it was set for; publishing a renamed Basic form without one is refused rather than left accepting the old pair. Changing nothing keeps the existing password working — an ordinary republish does not invalidate credentials you have already handed out.
File uploads
A file field does not carry bytes in the submission itself — it carries a reference, minted by its own upload endpoint:
POST /api/v1/forms/{formToken}/upload?field={fieldName}One call per file, multipart/form-data with a single file part — mirroring how the field behaves on the hosted page: select a file and it uploads immediately, before the rest of the form is necessarily filled in. A successful upload returns:
{ "fileId": "…", "filename": "resume.pdf", "sizeBytes": 48213 }Submit the returned fileId as the field's value — a plain string, or a string[] of them when the field's Allow multiple files option is on — never the file itself. The submission ingress resolves each id before the run starts, so a workflow reads the field's real value as:
{
"filename": "resume.pdf",
"contentType": "application/pdf",
"sizeBytes": 48213,
"text": "…extracted text…"
}text is extracted once, at upload time: .txt/.csv/.md/.html/.xml are read directly, .pdf/.docx/.doc/.xlsx/.xls are parsed, and anything else (images included) falls back to a short description rather than failing the upload.
Two settings live on the field itself:
| Setting | Effect |
|---|---|
| Accepted file types | A comma-separated extension list (.pdf,.docx,.csv). Empty accepts any extension. |
| Allow multiple files | Off by default. On, the field submits an array and the upload endpoint is called once per file. |
| Status | Meaning |
|---|---|
201 | Stored — body is the fileId/filename/sizeBytes shown above. |
400 | field names no file field on this form. |
422 | The file was rejected — wrong extension, over the size limit, or storage failed. |
429 | Rate limit — the same one submissions share. |
Upload size is a platform setting, not a per-form one
Every upload is capped at the same size regardless of the field's own configuration — 16 MB by
default. A rejected file's 422 response names the real, current limit, so a form need not
duplicate the number anywhere.
An upload nobody ever submits is deleted after 2 hours. Once a submission actually uses it, it is kept for 30 days from that submission — long enough to outlive a run parked on a Human Review node — after which it is deleted along with its stored bytes. Both windows, like the size limit, are platform settings rather than something a form author configures.
Submission responses
A submission is validated against the declared fields in the request, so an incomplete form reports its errors on the page rather than starting a run that fails where nobody is looking.
| Status | Meaning |
|---|---|
202 | Accepted — body is { "status": "received" }. The run proceeds in the background. |
400 | A field failed validation — issues names each one — or the graph behind the form is empty. |
401 | Credentials required, or wrong. |
403 | Signed in, but not a member of the workflow's organization. |
404 | No published form matches that token — including an unpublished or deleted workflow. |
413 | Body exceeded the 1 MB limit. |
429 | Rate limit, or the concurrency ceiling. |
A submitter never sees the run's result
The acknowledgment is immediate and static: no execution id, no polling handle, nothing that waits for the run. A workflow can park 30 days on a Human Review node, which makes "wait for the result" the wrong contract for a browser tab. If the submitter needs an answer, have the graph send them one.
Submitted values arrive under body, keyed by each field's name, so a form-driven graph reads {{ trigger.body.email }} — the same envelope the webhook path uses.
Recurrence
Add a Recurrence node and Elizon starts the workflow on a schedule of its own — no caller, and nothing for you to host.
Two modes:
- Cron — a cron expression plus an IANA time zone (for example
Europe/Sofia). Use this whenever you need a specific wall-clock time. - Interval — "every N minutes/hours/days". Intervals are aligned to the Unix epoch, not to the moment you published, so "every 1 day" fires at
00:00in the configured zone rather than at publish time.
The schedule is created, updated, or removed when you publish or unpublish — see Publishing. A misconfigured schedule (an empty cron string, an unrecognized time zone) fails the publish outright, with a message naming the problem, rather than leaving you with a "published" workflow that can never fire.
Scheduled runs carry an empty trigger payload, so a graph that depends on {{ trigger.* }} will not work on this path.
Where to go next
- Workflows API reference — request and response shapes.
- Publishing — the prerequisite for all four triggers.