Docs / Strand / getting-started/trigger-over-http

Trigger a workflow over HTTP

Strand has no client SDK, and for triggering it doesn't need one: starting a workflow from your own code is a single authenticated POST. This page shows that call, with copy-paste snippets in curl, Python, JavaScript, and Go — and where to reach for event-driven triggering instead.

Before you start#

The call is asynchronous: a 200 means the run was queued, not that it finished. You get a run_id back to check the result (see Check the result), and the workflow itself delivers its output through its connector nodes.

Run a workflow#

code

POST https://app.tendrl.com/strand/api/workflows/{workflow_id}/run

Use this whenever your code knows which workflow to start. No setup beyond the API key. The body is {"data": { ... }}, where data is the event your workflow receives — shape it like the real thing, because your nodes read from it via templating.

bash

curl -X POST https://app.tendrl.com/strand/api/workflows/YOUR_WORKFLOW_ID/run \
  -H "Authorization: Bearer $STRAND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"data": {"temperature": 22.5, "device": "sensor-1"}}'

Response:

json

{
  "message": "Workflow execution queued",
  "run_id": "542cab88_4ab4_49bc_ae16_bc0b05731fc2",
  "workflow_version_id": "1f9f7586_c749_436e_b4e2_6b12f7204ab1",
  "version": 1
}

The same call in other languages:

python


resp = requests.post(
    "https://app.tendrl.com/strand/api/workflows/YOUR_WORKFLOW_ID/run",
    headers={"Authorization": f"Bearer {os.environ['STRAND_API_KEY']}"},
    json={"data": {"temperature": 22.5, "device": "sensor-1"}},
)
resp.raise_for_status()
run_id = resp.json()["run_id"]
javascript

const resp = await fetch(
  "https://app.tendrl.com/strand/api/workflows/YOUR_WORKFLOW_ID/run",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.STRAND_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ data: { temperature: 22.5, device: "sensor-1" } }),
  },
);
if (!resp.ok) throw new Error(`Strand trigger failed: ${resp.status}`);
const { run_id } = await resp.json();
go

body := strings.NewReader(`{"data": {"temperature": 22.5, "device": "sensor-1"}}`)
req, _ := http.NewRequest("POST",
    "https://app.tendrl.com/strand/api/workflows/YOUR_WORKFLOW_ID/run", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("STRAND_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)

Rate limit: 60 requests per minute per account.

Event-driven triggering (no HTTP call)#

If the goal is "a device reading or a message should start a workflow," you usually don't make this call at all — you let the event route itself by tags. Give a workflow tags and turn on Expose to Contact, and any Contact message whose tags match starts it automatically. One message can start several workflows this way, with no code on your side. See Trigger from Contact.

Use the direct /run call above when your own code is the trigger (a cron job, a backend event, another service). Use Contact tags when a device or message is the trigger.

Check the result#

Both paths return a 200 as soon as the run is queued. To see what happened:

Responses and errors#

Status Meaning
200 Run queued. Not a guarantee the workflow finished — check the run.
401 Missing or invalid API key.
422 Body is missing the required data field.
403 The API key's role lacks run permission, or a plan limit was reached.
503 The run queue is temporarily full. Retry after a short wait; existing runs keep processing.

Next#