Stacklink
Workflows

TypeScript and Python

Create, test and run a workflow from your own code.

You’ll create a tiny workflow that returns a ticket ID. It needs no apps, so you can run it right away. It shows up on the canvas like any other workflow.

Install the SDK

You need Node.js 22+ or Python 3.10+, and a project key with workflows:read, workflows:manage, workflows:run, sessions:create and sessions:read. Set it as STACKLINK_API_KEY in your server environment. Keep it out of browser code and source control.

npm install @stacklink/sdk@0.6.4
npm install --save-dev tsx
python3 -m venv .venv
source .venv/bin/activate
pip install stacklink-sdk==0.6.4

Create it and test it

Save this file, or download it: TypeScript · Python. It checks the steps, saves a draft and runs a simulation.

ticket-intake.ts
import { Stacklink, type WorkflowDefinitionInput } from '@stacklink/sdk';

const stacklink = new Stacklink({
  apiKey: process.env.STACKLINK_API_KEY!,
  baseUrl: process.env.STACKLINK_API_URL ?? 'https://api.stacklink.in',
});
const definition: WorkflowDefinitionInput = {
  nodes: [
    { id: 'start', name: 'Ticket received', type: 'start', config: {},
      position: { x: 0, y: 0 } },
    { id: 'result', name: 'Return ticket ID', type: 'return',
      config: { value: { kind: 'input', path: ['ticketId'] } },
      position: { x: 340, y: 0 } },
  ],
  edges: [{ id: 'start-result', source: 'start', target: 'result' }],
  inputSchema: { type: 'object', properties: { ticketId: { type: 'string' } },
    required: ['ticketId'] },
};

async function observe(workflowId: string, runId: string) {
  for (let attempt = 0; attempt < 30; attempt++) {
    const run = await stacklink.workflows.getRun(workflowId, runId);
    if (!['queued', 'running', 'retrying'].includes(run.status)) return run;
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
  return stacklink.workflows.getRun(workflowId, runId);
}

async function main() {
  const checked = await stacklink.workflows.validate(definition);
  if (!checked.valid) throw new Error(JSON.stringify(checked.issues));
  const workflow = process.env.STACKLINK_WORKFLOW_ID
    ? await stacklink.workflows.get(process.env.STACKLINK_WORKFLOW_ID)
    : await stacklink.workflows.create({
        name: 'Docs · Ticket intake', source: 'visual', definition,
      });
  // Demo identities. Derive these from your authenticated user in a real app.
  const session = await stacklink.create({ tenantId: 'demo-company', userId: 'demo-user' });
  console.log({ workflowId: workflow.id, sessionId: session.id });
  const test = await stacklink.workflows.run(workflow.id, {
    sessionId: session.id, jobKey: `docs-test-${workflow.revision}`,
    revision: workflow.revision, purpose: 'test', mode: 'simulation',
    input: { ticketId: 'ticket-42' },
  });
  const tested = await observe(workflow.id, test.id);
  console.log({ testRunId: test.id, status: tested.status, output: tested.output });
  if (tested.status !== 'completed') {
    throw new Error('Test has not completed. Inspect its saved run before publishing.');
  }
  if (!process.argv.includes('--publish')) {
    console.log('Draft kept. Review it in Workflows before running with --publish.');
    return;
  }
  await stacklink.workflows.publish(workflow.id, workflow.revision);
  const run = await stacklink.workflows.run(workflow.id, {
    sessionId: session.id, jobKey: 'docs-ticket-42', purpose: 'operational',
    input: { ticketId: 'ticket-42' },
  });
  const result = await observe(workflow.id, run.id);
  console.log({ runId: run.id, status: result.status, output: result.output });
  // Waiting or still running is not failure. Keep the IDs and inspect later.
}
main().catch(error => { console.error(error); process.exitCode = 1; });
ticket_intake.py
import os
import sys
import time
from stacklink import Stacklink

stacklink = Stacklink(
    api_key=os.environ["STACKLINK_API_KEY"],
    base_url=os.environ.get("STACKLINK_API_URL", "https://api.stacklink.in"),
)
definition = {
    "nodes": [
        {"id": "start", "name": "Ticket received", "type": "start", "config": {},
         "position": {"x": 0, "y": 0}},
        {"id": "result", "name": "Return ticket ID", "type": "return",
         "config": {"value": {"kind": "input", "path": ["ticketId"]}},
         "position": {"x": 340, "y": 0}},
    ],
    "edges": [{"id": "start-result", "source": "start", "target": "result"}],
    "inputSchema": {"type": "object", "properties": {"ticketId": {"type": "string"}},
                    "required": ["ticketId"]},
}


def observe(workflow_id, run_id):
    for _ in range(30):
        run = stacklink.workflows.get_run(workflow_id, run_id)
        if run["status"] not in ("queued", "running", "retrying"):
            return run
        time.sleep(1)
    return stacklink.workflows.get_run(workflow_id, run_id)


checked = stacklink.workflows.validate(definition)
if not checked["valid"]:
    raise RuntimeError(checked["issues"])
workflow_id = os.environ.get("STACKLINK_WORKFLOW_ID")
workflow = (stacklink.workflows.get(workflow_id) if workflow_id else
            stacklink.workflows.create("Docs · Ticket intake", definition, source="visual"))
# Demo identities. Derive these from your authenticated user in a real app.
session = stacklink.sessions.create_bound({
    "externalTenantId": "demo-company", "externalUserId": "demo-user",
})
print({"workflowId": workflow["id"], "sessionId": session.id})
test = stacklink.workflows.run(
    workflow["id"], session.id, f"docs-test-{workflow['revision']}",
    revision=workflow["revision"], purpose="test", mode="simulation",
    input={"ticketId": "ticket-42"},
)
tested = observe(workflow["id"], test["id"])
print({"testRunId": test["id"], "status": tested["status"], "output": tested["output"]})
if tested["status"] != "completed":
    raise RuntimeError("Test has not completed. Inspect its saved run before publishing.")
if "--publish" in sys.argv:
    stacklink.workflows.publish(workflow["id"], workflow["revision"])
    run = stacklink.workflows.run(
        workflow["id"], session.id, "docs-ticket-42", purpose="operational",
        input={"ticketId": "ticket-42"},
    )
    result = observe(workflow["id"], run["id"])
    print({"runId": run["id"], "status": result["status"], "output": result["output"]})
else:
    print("Draft kept. Review it in Workflows before running with --publish.")
# Waiting or still running is not failure. Keep the IDs and inspect later.
npx tsx ticket-intake.ts
python ticket_intake.py

You should see status: "completed" and output: "ticket-42". The draft is now in Workflows → Docs · Ticket intake.

Publish and run it

When you’re happy, run it again with --publish. Set STACKLINK_WORKFLOW_ID to the ID it printed, so it reuses the same workflow.

STACKLINK_WORKFLOW_ID="YOUR_SAVED_ID" npx tsx ticket-intake.ts --publish
STACKLINK_WORKFLOW_ID="YOUR_SAVED_ID" python ticket_intake.py --publish

A run that has started isn’t finished yet

run() returns the run after Stacklink accepts it. It may still be queued. The example checks back for up to 30 seconds. Waiting for an approval isn’t a failure; keep the run ID and look again later.

Common next steps

To…Do this
Change the stepsget() the draft, then update() it with its revision
Retry a request that timed outSend it again with the same jobKey, input and caller. You get the same run back.
Start new workUse a new job key
Run it on a scheduleSave a trigger, then publish
Keep it code-onlyCreate it with source: "code"; the canvas becomes read-only
Production and staging
EnvironmentSTACKLINK_API_URLKey
Production (default)https://api.stacklink.inProduction project key
Staginghttps://api-staging.stacklink.inSeparate staging key

The session decides whose accounts and permissions a run uses. For workflows your customers build, use the embedded access model.

Next: Ask Claude or Codex → · All calls →

On this page