Studio API¶
Audience: Developers · Tags:
api,reference
Snap Studio is SnapLink's guided, AI-assisted flow builder: five endpoints under /flow-designer/studio/* that take a plain-English prompt, turn it into an editable plan, let the user (or your client) edit the plan's field values, validate it, save it as a real flow, and run it once to confirm it works — with a plain-English explanation at every step.
AI doctrine: SnapLink's assistant drafts flows for you. It never turns anything on, never runs anything, and never sees your credentials. You review and approve every flow before it exists. Every Studio endpoint is a proposal or a read; the platform only creates or activates anything when you call studio/approve, and even then only after server-side validation passes.
All five routes require Authorization: Bearer $TOKEN. Envelope: { "data": ... } only — no ok key on success, same as the rest of /flow-designer and /marketplace (see API Reference → the envelope). Errors use the API-wide shape: { "ok": false, "error": { "code", "message", "details"? } }.
The plan shape¶
A StudioPlan is not a raw flow definition — it's a friendly, editable intermediate form: a name, a plain-English summary, an ordered list of steps (one trigger step first, then one or more action steps), and optional clarifying questions the planner couldn't answer from your prompt.
interface StudioPlan {
engine: 'rules' | 'llm'; // which planner produced it
name: string;
summary: string;
steps: StudioStep[];
questions: string[];
}
interface StudioStep {
id: string;
kind: 'trigger' | 'action';
title: string;
description: string;
connectorKey: string; // 'webhook' | 'rest_sdk' | 'email' — see "Untrusted input" below
capability: string; // 'webhook' | 'request' | 'send_email'
fields: StudioField[];
}
interface StudioField {
key: string;
label: string;
value: string; // what actually gets sent — edit this
required: boolean;
type: 'text' | 'email' | 'url' | 'textarea';
help?: string;
}
engine is "llm" when SnapLink's configured LLM planner produced the plan, or "rules" when the deterministic fallback planner did (used whenever no LLM is configured, or the LLM call fails — Studio always returns a usable plan either way). The message an action sends (the text field's value) is fixed text the user writes or edits — Studio does not insert live trigger data into it. There is no payload templating/mapping syntax.
POST /flow-designer/studio/plan¶
Turns a prompt into a plan. Nothing is saved yet.
curl -X POST https://api.snaplink.cloudsolutionaccelerators.com/api/v1/flow-designer/studio/plan \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"prompt":"email me when my link gets a message"}'
{
"data": {
"plan": {
"engine": "rules",
"name": "email me when my link gets a message",
"summary": "Here is the plan: whenever your SnapLink link is triggered, I will send an email. Look over each step below — you can change anything — then check and save it.",
"steps": [
{
"id": "trigger",
"kind": "trigger",
"title": "When: your SnapLink link is triggered",
"description": "SnapLink gives this flow its own private web link. Whenever that link is called — by you, a form, or another app — the flow runs.",
"connectorKey": "webhook",
"capability": "webhook",
"fields": []
},
{
"id": "action-1",
"kind": "action",
"title": "Then: send an email",
"description": "SnapLink sends an email with your message.",
"connectorKey": "email",
"capability": "send_email",
"fields": [
{ "key": "to", "label": "Send it to (email address)", "value": "", "required": true, "type": "email", "help": "The inbox that should receive the message." },
{ "key": "subject", "label": "Email subject", "value": "A message from your SnapLink flow", "required": false, "type": "text", "help": "The subject line of the email. Optional." },
{ "key": "text", "label": "The message to send", "value": "Hello! Your SnapLink flow just ran.", "required": true, "type": "textarea", "help": "This exact message is sent each time the flow runs. You can change it any time." }
]
}
],
"questions": [
"What email address should the message go to? (Fill in \"Send it to\" below.)"
]
}
}
}
Note the to field's value is empty and questions flags it — the prompt didn't include an email address, so the rules planner drafted the shape of the flow but left the destination for you to fill in. Had the prompt been "email jane@example.com when my link gets a message", to.value would already be "jane@example.com" and questions would be empty.
400 VALIDATION_ERROR if prompt is missing, empty, or over 2000 characters.
POST /flow-designer/studio/check¶
Validates a plan (yours, or one returned from studio/plan and then edited) against the target workspace, without saving anything. Use this after editing field values to see whether the flow would actually work.
curl -X POST https://api.snaplink.cloudsolutionaccelerators.com/api/v1/flow-designer/studio/check \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"workspaceId": "ws_51ac9d",
"plan": {
"engine": "rules",
"name": "email me when my link gets a message",
"summary": "Here is the plan: whenever your SnapLink link is triggered, I will send an email.",
"steps": [
{ "id": "trigger", "kind": "trigger", "title": "When: your SnapLink link is triggered", "description": "...", "connectorKey": "webhook", "capability": "webhook", "fields": [] },
{ "id": "action-1", "kind": "action", "title": "Then: send an email", "description": "...", "connectorKey": "email", "capability": "send_email", "fields": [
{ "key": "to", "label": "Send it to (email address)", "value": "jane@example.com", "required": true, "type": "email" },
{ "key": "subject", "label": "Email subject", "value": "A message from your SnapLink flow", "required": false, "type": "text" },
{ "key": "text", "label": "The message to send", "value": "Hello! Your SnapLink flow just ran.", "required": true, "type": "textarea" }
] }
],
"questions": []
}
}'
All problems resolved:
{ "data": { "ok": true, "problems": [], "explanation": "When a webhook trigger fires, this flow will email: send email." } }
Still broken (e.g. to left empty):
{
"data": {
"ok": false,
"problems": [
{ "stepId": "action-1", "field": "to", "message": "Then: send an email: please fill in \"Send it to (email address)\"." }
],
"explanation": "When a webhook trigger fires, this flow will email: send email."
}
}
check also silently provisions the connectors your plan's actions use (installs + activates them in the workspace if they aren't already) so that what check reports matches what approve will actually do — this never touches credentials, only the workspace-scoped install registry.
400 VALIDATION_ERROR if workspaceId is missing. If the request body's plan isn't well-formed at all (see Untrusted input, below), this returns 400 STUDIO_PLAN_MALFORMED rather than a problems array — malformed and merely-incomplete are different failure modes.
POST /flow-designer/studio/approve¶
Saves an approved plan as a real, active flow: creates the flow (or reuses an existing non-archived flow with the same name in the workspace), compiles the plan into a flow definition, creates a draft, and immediately promotes it. This is the only Studio call that writes anything.
curl -X POST https://api.snaplink.cloudsolutionaccelerators.com/api/v1/flow-designer/studio/approve \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"workspaceId": "ws_51ac9d",
"name": "Email me on link hit",
"plan": { "...": "the same edited plan you passed to studio/check" }
}'
{ "data": { "flowId": "flow_9c21f0", "flowName": "Email me on link hit" } }
name is optional — omit it to use the plan's own name. HTTP status is 201.
If the plan still has unresolved problems, approve re-runs the same check studio/check does and refuses to save:
{
"ok": false,
"error": {
"code": "STUDIO_PLAN_INVALID",
"message": "The flow has problems that need fixing first.",
"details": {
"problems": [
{ "stepId": "action-1", "field": "to", "message": "Then: send an email: please fill in \"Send it to (email address)\"." }
]
}
}
}
400. Fix the flagged fields client-side and re-submit — there's no partial-save.
POST /flow-designer/studio/test-run¶
Runs the now-saved flow once through the real execution engine, then returns the same plain-English explanation as studio/runs/:runId/explain (below). This is a genuine send — the trigger fires the flow for real (email is actually sent, REST call actually made) — not a dry run or simulation.
curl -X POST https://api.snaplink.cloudsolutionaccelerators.com/api/v1/flow-designer/studio/test-run \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"workspaceId":"ws_51ac9d","flowId":"flow_9c21f0"}'
Success:
{
"data": {
"runId": "run_5e10aa",
"status": "succeeded",
"headline": "Success — your flow ran from start to finish.",
"steps": [
{ "title": "Step 1 — Sending an email", "status": "done", "detail": "This step finished without any problems." }
],
"problem": null
}
}
201.
Failure (e.g. the email connector isn't configured):
{
"data": {
"runId": "run_1adf22",
"status": "failed",
"headline": "Your flow ran, but step 1 hit a problem.",
"steps": [
{ "title": "Step 1 — Sending an email", "status": "failed", "detail": "This step failed because it couldn't sign in — a credential or setting (like SMTP for email) is missing or wrong." }
],
"problem": {
"message": "Sending an email did not work: it couldn't sign in — a credential or setting (like SMTP for email) is missing or wrong.",
"suggestion": "Re-connect the connector — its credential is missing, expired, or invalid."
}
}
}
GET /flow-designer/studio/runs/:runId/explain¶
Re-fetches the same explanation test-run returns, for any run — not just ones Studio created. Read-only; safe to poll.
curl https://api.snaplink.cloudsolutionaccelerators.com/api/v1/flow-designer/studio/runs/run_5e10aa/explain \
-H "Authorization: Bearer $TOKEN"
{ "data": { "runId": "run_5e10aa", "status": "succeeded", "headline": "Success — your flow ran from start to finish.", "steps": [ { "title": "Step 1 — Sending an email", "status": "done", "detail": "This step finished without any problems." } ], "problem": null } }
Untrusted input: server-side re-validation¶
Every plan your client sends to studio/check or studio/approve is treated as untrusted input, even the exact plan you got back from studio/plan seconds earlier — the server re-validates on every call, not just at generation time. Two layers apply:
- Schema + whitelist (
STUDIO_PLAN_MALFORMED,400). The plan must match theStudioPlanzod schema, and every step'sconnectorKey/capabilitypair must be one of the three the platform accepts: - trigger:
webhook+webhook - action:
rest_sdk+request, oremail+send_email
Any other connector or capability — including real, installed connectors like slack or hubspot — is rejected outright as malformed. Studio-authored flows are deliberately limited to this trigger + REST/email action set; there's no way to point a Studio plan at an arbitrary connector.
{ "ok": false, "error": { "code": "STUDIO_PLAN_MALFORMED", "message": "This plan is not something Snap Studio can build." } }
-
Structural invariants, enforced alongside the schema: the first step must be the
triggerkind and must be the webhook trigger; every step after it must be anactionusing one of the two allowed action pairs; there must be at least one action. A plan that violates these (steps out of order, no actions, a second trigger, etc.) also fails asSTUDIO_PLAN_MALFORMED. -
Content-level problems (
STUDIO_PLAN_INVALID,400,approveonly). A structurally well-formed plan can still be invalid in content — a required field left blank, a connector not installed in the workspace — surfaced as theproblemsarray shown above. This is the only error carryingerror.details.problems.
Field sizes are bounded server-side regardless of what the client sends: plan name ≤ 80 chars, summary ≤ 1000 chars, ≤ 6 steps, ≤ 10 fields per step, field value ≤ 5000 chars, help ≤ 300 chars, ≤ 5 questions of ≤ 200 chars each.
End-to-end example: signup → plan → edit → check → approve → test-run¶
Full scripted walkthrough with bash + curl + jq. This builds the exact "email me when my link gets a message" flow shown above, filling in the missing email address before saving.
#!/usr/bin/env bash
set -euo pipefail
BASE="https://api.snaplink.cloudsolutionaccelerators.com/api/v1"
# 1. Sign up (skip this and use /auth/login if you already have an account).
SIGNUP=$(curl -s -X POST "$BASE/auth/signup" -H "Content-Type: application/json" \
-d '{"email":"jane+studio@example.com","password":"correct-horse-battery"}')
TOKEN=$(echo "$SIGNUP" | jq -r '.data.token')
# 2. Create a workspace.
WORKSPACE=$(curl -s -X POST "$BASE/workspaces" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Jane Solo"}')
WS=$(echo "$WORKSPACE" | jq -r '.data.workspace.id')
# 3. Ask Snap Studio for a plan.
PLAN_RES=$(curl -s -X POST "$BASE/flow-designer/studio/plan" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"prompt":"email me when my link gets a message"}')
# 4. Edit the plan's field values in the JSON — fill in the destination email
# the rules planner left blank (jq walks steps/fields by key, not index,
# so this still works if the planner ever reorders fields).
EDITED_PLAN=$(echo "$PLAN_RES" | jq '
.data.plan
| .steps |= map(
if .connectorKey == "email"
then .fields |= map(if .key == "to" then .value = "jane+studio@example.com" else . end)
else . end
)
')
# 5. Check the edited plan against the workspace.
CHECK=$(curl -s -X POST "$BASE/flow-designer/studio/check" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "$(jq -n --arg ws "$WS" --argjson plan "$EDITED_PLAN" '{workspaceId: $ws, plan: $plan}')")
if [ "$(echo "$CHECK" | jq -r '.data.ok')" != "true" ]; then
echo "Plan still has problems:" >&2
echo "$CHECK" | jq '.data.problems' >&2
exit 1
fi
# 6. Approve — this is the only call that actually saves + activates the flow.
APPROVE=$(curl -s -X POST "$BASE/flow-designer/studio/approve" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "$(jq -n --arg ws "$WS" --argjson plan "$EDITED_PLAN" \
'{workspaceId: $ws, plan: $plan, name: "Email me on link hit"}')")
FLOW_ID=$(echo "$APPROVE" | jq -r '.data.flowId')
echo "Saved flow: $FLOW_ID"
# 7. Test-run it once through the real engine.
TEST_RUN=$(curl -s -X POST "$BASE/flow-designer/studio/test-run" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "$(jq -n --arg ws "$WS" --arg flow "$FLOW_ID" '{workspaceId: $ws, flowId: $flow}')")
echo "$TEST_RUN" | jq '.data.headline, .data.steps'
# 8. Re-read the same explanation later (e.g. after refreshing a results page).
RUN_ID=$(echo "$TEST_RUN" | jq -r '.data.runId')
curl -s "$BASE/flow-designer/studio/runs/$RUN_ID/explain" -H "Authorization: Bearer $TOKEN" | jq .data
Equivalent step 3–4 (fetch plan, edit a field) in Node, if you'd rather not shell out to jq:
const BASE = "https://api.snaplink.cloudsolutionaccelerators.com/api/v1";
const planRes = await fetch(`${BASE}/flow-designer/studio/plan`, {
method: "POST",
headers: { Authorization: `Bearer ${token}`, "content-type": "application/json" },
body: JSON.stringify({ prompt: "email me when my link gets a message" }),
});
const { data } = await planRes.json();
const plan = data.plan;
for (const step of plan.steps) {
if (step.connectorKey === "email") {
const toField = step.fields.find((f) => f.key === "to");
if (toField) toField.value = "jane+studio@example.com";
}
}
// plan is now ready to pass to studio/check, then studio/approve.
Related: API Reference · Webhooks · AI flow drafting · Creating flows