Async webhook worker (complete)
Queue-friendly webhook automation: trigger with sg_aios_…, then poll and export
with stackgen_…. Handles the common 202 without session_id case.
How to run · Minimal · Complete script
| Step | SDK call |
|---|---|
| 1. Trigger | trigger_webhook(payload_string) |
| 2. Resolve session | wait_for_webhook_session(webhook_id, invocation_id) when needed |
| 3. Resume | get_session |
| 4. Write-up | export_session(..., parts="report") |
How to run
pip install stackgen-sdk
export STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
export STACKGEN_WEBHOOK_TOKEN=sg_aios_…
python examples/recipes/webhook-async-worker.py --start-only
python examples/recipes/webhook-async-worker.pynpm install stackgen-sdk tsx
export STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
export STACKGEN_WEBHOOK_TOKEN=sg_aios_…
npx tsx examples/recipes/webhook-async-worker.ts --start-only
npx tsx examples/recipes/webhook-async-worker.tsexport STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
export STACKGEN_WEBHOOK_TOKEN=sg_aios_…
go run ./examples/go/recipes/webhook-async-worker.go --start-only
go run ./examples/go/recipes/webhook-async-worker.goMinimal
import json
from stackgen import StackgenClient, StackgenConfig
client = StackgenClient(
StackgenConfig(
base_url="https://app.stackgen.com",
api_token="stackgen_…",
webhook_token="sg_aios_…",
workspace_id="<workspace-uuid>",
)
)
trigger = client.aiden.trigger_webhook(
json.dumps({"query": "Investigate the checkout CPU alert in prod", "correlation_id": "corr-123"})
)
session_id = trigger.get("session_id") or client.aiden.wait_for_webhook_session(
trigger["webhook_id"],
trigger["invocation_id"],
dispatch_run_id=str(trigger.get("run_id") or ""),
)
export = client.aiden.export_session(session_id, parts="report")import { StackgenClient, StackgenConfig } from "stackgen-sdk";
const client = new StackgenClient(
new StackgenConfig({
baseUrl: "https://app.stackgen.com",
apiToken: "stackgen_…",
webhookToken: "sg_aios_…",
workspaceId: "<workspace-uuid>",
}),
);
const trigger = await client.aiden.triggerWebhook(
JSON.stringify({
query: "Investigate the checkout CPU alert in prod",
correlation_id: "corr-123",
}),
);
let sessionId = String(trigger.session_id ?? "");
if (!sessionId) {
sessionId = await client.aiden.waitForWebhookSession(
String(trigger.webhook_id),
String(trigger.invocation_id),
{ dispatchRunId: String(trigger.run_id ?? "") },
);
}
await client.aiden.exportSession(sessionId, { parts: "report" });trigger, err := client.Aiden.TriggerWebhook(ctx,
`{"query":"Investigate the checkout CPU alert in prod","correlation_id":"corr-123"}`)
if err != nil { return err }
sessionID, _ := trigger["session_id"].(string)
if sessionID == "" {
sessionID, err = client.Aiden.WaitForWebhookSession(ctx,
trigger["webhook_id"].(string),
trigger["invocation_id"].(string),
fmt.Sprint(trigger["run_id"]),
nil,
)
if err != nil { return err }
}
_, err = client.Aiden.ExportSession(ctx, sessionID, "report", "json")
return errComplete script
- Python:
examples/recipes/webhook-async-worker.py - TypeScript:
examples/recipes/webhook-async-worker.ts - Go:
examples/go/recipes/webhook-async-worker.go
#!/usr/bin/env python3
"""Complete async webhook worker (obfuscated sample ids).
Trigger with the webhook token, then poll/run control with the API token.
Shows the dual-credential pattern queue workers need.
pip install stackgen-sdk
export STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
export STACKGEN_WEBHOOK_TOKEN=sg_aios_…
python webhook-async-worker.py --start-only
python webhook-async-worker.py
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from stackgen import StackgenClient, StackgenConfig
from stackgen.errors import StackgenError
def _env(*keys: str) -> str:
for key in keys:
value = os.environ.get(key, "").strip()
if value:
return value
return ""
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--query",
default="Investigate the checkout CPU alert in prod",
)
parser.add_argument("--correlation-id", default="corr-123")
parser.add_argument("--timeout-seconds", type=int, default=1800)
parser.add_argument("--poll-interval-seconds", type=int, default=5)
parser.add_argument("--start-only", action="store_true")
args = parser.parse_args()
base = _env("STACKGEN_URL")
token = _env("STACKGEN_TOKEN")
org = _env("STACKGEN_WORKSPACE_ID", "STACKGEN_PROJECT", "STACKGEN_PROJECT_ID")
webhook = _env("STACKGEN_WEBHOOK_TOKEN", "WEBHOOK_TOKEN")
if not base or not token or not org or not webhook:
raise SystemExit(
"set STACKGEN_URL, STACKGEN_TOKEN, STACKGEN_WORKSPACE_ID, "
"and STACKGEN_WEBHOOK_TOKEN"
)
client = StackgenClient(
StackgenConfig(
base_url=base,
api_token=token,
workspace_id=org,
webhook_token=webhook,
timeout_seconds=args.timeout_seconds,
poll_interval_seconds=args.poll_interval_seconds,
)
)
# 1) Fire-and-forget trigger (webhook token). Body may be plain text or JSON.
payload = json.dumps(
{
"query": args.query,
"message": args.query,
"correlation_id": args.correlation_id,
}
)
triggered = client.aiden.trigger_webhook(payload)
print("trigger:", json.dumps(triggered, indent=2, default=str))
if args.start_only:
return 0
webhook_id = str(triggered.get("webhook_id") or "")
invocation_id = str(triggered.get("invocation_id") or "")
session_id = triggered.get("session_id")
dispatch_run_id = str(triggered.get("run_id") or triggered.get("id") or "")
# 2) If session is missing (HTTP 202), poll until session_id appears (API token)
if not session_id:
if not webhook_id or not invocation_id:
print(
"error: trigger missing webhook_id/invocation_id and session_id",
file=sys.stderr,
)
return 1
session_id = client.aiden.wait_for_webhook_session(
webhook_id,
invocation_id,
dispatch_run_id=dispatch_run_id,
)
print(f"resolved session_id={session_id}", file=sys.stderr)
session_id = str(session_id)
detail = client.aiden.get_session(session_id)
print("get_session:", json.dumps(detail, indent=2, default=str))
export = client.aiden.export_session(session_id, parts="report")
print("export:", json.dumps(export, indent=2, default=str))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except StackgenError as exc:
print(f"error: {exc}", file=sys.stderr)
raise SystemExit(1)#!/usr/bin/env npx tsx
/**
* Complete async webhook worker (obfuscated sample ids).
*
* Trigger with webhook token, resolve session, then export with API token.
*
* npm install stackgen-sdk tsx
* export STACKGEN_URL=https://app.stackgen.com
* export STACKGEN_TOKEN=stackgen_…
* export STACKGEN_WORKSPACE_ID=<workspace-uuid>
* export STACKGEN_WEBHOOK_TOKEN=sg_aios_…
*
* npx tsx webhook-async-worker.ts --start-only
* npx tsx webhook-async-worker.ts
*/
import { StackgenClient, StackgenConfig, StackgenError } from "stackgen-sdk";
function env(...keys: string[]): string {
for (const key of keys) {
const value = (process.env[key] ?? "").trim();
if (value) {
return value;
}
}
return "";
}
function parseArgs(argv: string[]) {
const out = {
query: "Investigate the checkout CPU alert in prod",
correlationId: "corr-123",
timeoutSeconds: 1800,
pollIntervalSeconds: 5,
startOnly: false,
};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === "--start-only") {
out.startOnly = true;
} else if (a === "--query") {
out.query = argv[++i] ?? out.query;
} else if (a === "--correlation-id") {
out.correlationId = argv[++i] ?? out.correlationId;
} else if (a === "--timeout-seconds") {
out.timeoutSeconds = Number(argv[++i] ?? out.timeoutSeconds);
} else if (a === "--poll-interval-seconds") {
out.pollIntervalSeconds = Number(argv[++i] ?? out.pollIntervalSeconds);
}
}
return out;
}
async function main(): Promise<number> {
const args = parseArgs(process.argv.slice(2));
const baseUrl = env("STACKGEN_URL");
const apiToken = env("STACKGEN_TOKEN");
const orgId = env("STACKGEN_WORKSPACE_ID", "STACKGEN_PROJECT", "STACKGEN_PROJECT_ID");
const webhookToken = env("STACKGEN_WEBHOOK_TOKEN", "WEBHOOK_TOKEN");
if (!baseUrl || !apiToken || !orgId || !webhookToken) {
console.error(
"set STACKGEN_URL, STACKGEN_TOKEN, STACKGEN_WORKSPACE_ID, and STACKGEN_WEBHOOK_TOKEN",
);
return 1;
}
const client = new StackgenClient(
new StackgenConfig({
baseUrl,
apiToken,
workspaceId: orgId,
webhookToken,
timeoutSeconds: args.timeoutSeconds,
pollIntervalSeconds: args.pollIntervalSeconds,
}),
);
const payload = JSON.stringify({
query: args.query,
message: args.query,
correlation_id: args.correlationId,
});
const triggered = await client.aiden.triggerWebhook(payload);
console.log("trigger:", JSON.stringify(triggered, null, 2));
if (args.startOnly) {
return 0;
}
const webhookId = String(triggered.webhook_id ?? "");
const invocationId = String(triggered.invocation_id ?? "");
let sessionId = String(triggered.session_id ?? "");
const dispatchRunId = String(triggered.run_id ?? triggered.id ?? "");
if (!sessionId) {
if (!webhookId || !invocationId) {
console.error(
"error: trigger missing webhook_id/invocation_id and session_id",
);
return 1;
}
sessionId = await client.aiden.waitForWebhookSession(
webhookId,
invocationId,
{ dispatchRunId },
);
console.error(`resolved session_id=${sessionId}`);
}
const detail = await client.aiden.getSession(sessionId);
console.log("get_session:", JSON.stringify(detail, null, 2));
const exported = await client.aiden.exportSession(sessionId, {
parts: "report",
});
console.log("export:", JSON.stringify(exported, null, 2));
return 0;
}
main().catch((err) => {
const msg = err instanceof StackgenError ? err.message : String(err);
console.error(`error: ${msg}`);
process.exit(1);
});// Async webhook worker: trigger with the webhook token, then export with the API token.
//
// Documented at https://appcd-dev.github.io/stackgen-sdk/code-samples/async-webhook-worker/
//
// export STACKGEN_URL=https://app.stackgen.com
// export STACKGEN_TOKEN=stackgen_…
// export STACKGEN_WORKSPACE_ID=<workspace-uuid>
// export STACKGEN_WEBHOOK_TOKEN=sg_aios_…
// go run ./examples/go/recipes/webhook-async-worker.go --start-only
// go run ./examples/go/recipes/webhook-async-worker.go
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
stackgen "github.com/appcd-dev/stackgen-sdk/clients/go"
)
func envOr(keys ...string) string {
for _, key := range keys {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
}
return ""
}
func text(m map[string]any, keys ...string) string {
for _, key := range keys {
if v, ok := m[key].(string); ok && strings.TrimSpace(v) != "" {
return strings.TrimSpace(v)
}
}
return ""
}
func main() {
query := flag.String("query", "Investigate the checkout CPU alert in prod", "Alert text")
correlationID := flag.String("correlation-id", "corr-123", "Correlation id")
timeoutSeconds := flag.Int("timeout-seconds", 1800, "Overall timeout")
pollInterval := flag.Int("poll-interval-seconds", 5, "Poll interval")
startOnly := flag.Bool("start-only", false, "Trigger and exit before waiting for a session")
flag.Parse()
base := envOr("STACKGEN_URL", "STACKGEN_BASE_URL")
token := envOr("STACKGEN_TOKEN", "STACKGEN_API_TOKEN")
org := envOr("STACKGEN_WORKSPACE_ID", "STACKGEN_PROJECT", "STACKGEN_PROJECT_ID")
webhook := envOr("STACKGEN_WEBHOOK_TOKEN", "WEBHOOK_TOKEN")
if base == "" || token == "" || org == "" || webhook == "" {
fmt.Fprintln(os.Stderr, "set STACKGEN_URL, STACKGEN_TOKEN, STACKGEN_WORKSPACE_ID, and STACKGEN_WEBHOOK_TOKEN")
os.Exit(2)
}
client, err := stackgen.NewClient(stackgen.Config{
BaseURL: base,
APIToken: token,
WorkspaceID: org,
WebhookToken: webhook,
TimeoutSeconds: *timeoutSeconds,
PollIntervalSeconds: *pollInterval,
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
ctx := context.Background()
payload, _ := json.Marshal(map[string]string{
"query": *query,
"message": *query,
"correlation_id": *correlationID,
})
triggered, err := client.Aiden.TriggerWebhook(ctx, string(payload))
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
raw, _ := json.MarshalIndent(triggered, "", " ")
fmt.Println("trigger:", string(raw))
if *startOnly {
return
}
sessionID := text(triggered, "session_id", "sessionId")
if sessionID == "" {
webhookID := text(triggered, "webhook_id", "webhookId")
invocationID := text(triggered, "invocation_id", "invocationId")
if webhookID == "" || invocationID == "" {
fmt.Fprintln(os.Stderr, "error: trigger missing webhook_id/invocation_id and session_id")
os.Exit(1)
}
sessionID, err = client.Aiden.WaitForWebhookSession(ctx, webhookID, invocationID, text(triggered, "run_id", "id"), nil)
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "resolved session_id=%s\n", sessionID)
}
detail, err := client.Aiden.GetSession(ctx, sessionID)
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
raw, _ = json.MarshalIndent(detail, "", " ")
fmt.Println("get_session:", string(raw))
exported, err := client.Aiden.ExportSession(ctx, sessionID, "report", "json")
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
raw, _ = json.MarshalIndent(exported, "", " ")
fmt.Println("export:", string(raw))
}Related: Async webhook CI, Async Ask worker, Webhook → session → artifact.