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 tsxpython3 -m venv .venv
source .venv/bin/activate
pip install stacklink-sdk==0.6.4Create it and test it
Save this file, or download it: TypeScript · Python. It checks the steps, saves a draft and runs a simulation.
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; });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.tspython ticket_intake.pyYou 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 --publishSTACKLINK_WORKFLOW_ID="YOUR_SAVED_ID" python ticket_intake.py --publishA 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 steps | get() the draft, then update() it with its revision |
| Retry a request that timed out | Send it again with the same jobKey, input and caller. You get the same run back. |
| Start new work | Use a new job key |
| Run it on a schedule | Save a trigger, then publish |
| Keep it code-only | Create it with source: "code"; the canvas becomes read-only |
Production and staging
| Environment | STACKLINK_API_URL | Key |
|---|---|---|
| Production (default) | https://api.stacklink.in | Production project key |
| Staging | https://api-staging.stacklink.in | Separate 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 →