Webhook → session only
Trigger a webhook and obtain session_id — without waiting for or
downloading an artifact. Use this when another system owns the download step, or you only
need the session id for logging or a follow-up API call.
Use client.aiden.trigger_webhook for a stateless trigger, then
wait_for_webhook_session when the 202 body omits session_id.
See Async webhook CI.
| Step | What the SDK does |
|---|---|
| 1. Trigger | POST /guild/api/v1/webhooks/trigger with webhook token |
| 2. Session | Use session_id from 202 when present; otherwise poll run detail |
Runnable CLI
Run the step-by-step script without --download-report:
#!/usr/bin/env python3
"""Webhook trigger → read session_id (optional artifact download).
Documented at https://appcd-dev.github.io/stackgen-sdk/code-samples/webhook-session-artifact/
Usage:
export WEBHOOK_TOKEN='sg_aios_…'
python webhook-wait-session.py \\
--base-url https://app.stackgen.com \\
--api-token stackgen_… \\
--org-id <project-uuid> \\
--json-body \\
--query 'P1 | checkout-api error rate above SLO'
# Also download session-report.md when the workflow finishes:
python webhook-wait-session.py … --download-report
"""
from __future__ import annotations
import argparse
import json
import sys
from stackgen import StackgenConfig, _http
from stackgen.aiden import sessions as session_helpers
from stackgen.errors import StackgenError
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base-url", required=True)
parser.add_argument("--api-token", required=True)
parser.add_argument("--org-id", required=True)
parser.add_argument("--webhook-token", default="", help="Or set WEBHOOK_TOKEN env")
parser.add_argument("--query", default="Test webhook from stackgen-sdk example")
parser.add_argument("--json-body", action="store_true")
parser.add_argument("--download-report", action="store_true")
parser.add_argument("--artifact-name", default="session-report.md")
parser.add_argument("--output-path", default="session-report.md")
parser.add_argument("--timeout-seconds", type=int, default=1800)
parser.add_argument("--poll-interval-seconds", type=int, default=5)
args = parser.parse_args()
import os
webhook_token = args.webhook_token or os.environ.get("WEBHOOK_TOKEN", "")
if not webhook_token:
print("error: set --webhook-token or WEBHOOK_TOKEN", file=sys.stderr)
return 2
config = StackgenConfig(
base_url=args.base_url,
api_token=args.api_token,
webhook_token=webhook_token,
org_id=args.org_id,
artifact_name=args.artifact_name,
output_path=args.output_path,
timeout_seconds=args.timeout_seconds,
poll_interval_seconds=args.poll_interval_seconds,
)
payload = (
json.dumps({"message": args.query, "query": args.query})
if args.json_body
else args.query
)
content_type = "application/json" if args.json_body else "text/plain"
url = f"{config.aiden_url()}/api/v1/webhooks/trigger{_http.query(config.org_id)}"
print(f"POST {url} content_type={content_type}", file=sys.stderr)
trigger = _http.request(
url,
config.webhook_token,
method="POST",
body=payload.encode("utf-8"),
content_type=content_type,
)
if not isinstance(trigger, dict):
raise StackgenError(f"unexpected trigger response: {trigger!r}")
print(json.dumps(trigger, indent=2))
session_id = _http.text(trigger.get("session_id"))
if not session_id:
raise StackgenError("trigger response missing session_id")
print(f"session_id={session_id}", file=sys.stderr)
if args.download_report:
session_helpers.wait_for_artifact(config, session_id, args.artifact_name)
path = session_helpers.download_artifact(
config, session_id, args.artifact_name, args.output_path
)
print(f"output_path={path}", file=sys.stderr)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except StackgenError as exc:
print(f"error: {exc}", file=sys.stderr)
raise SystemExit(1)/**
* Webhook trigger → read session_id (optional artifact download).
*
* Docs: https://appcd-dev.github.io/stackgen-sdk/code-samples/webhook-session-artifact/
*
* Usage:
* WEBHOOK_TOKEN='sg_aios_…' npx tsx webhook-wait-session-step.ts \
* --base-url https://app.stackgen.com \
* --api-token stackgen_… \
* --org-id <project-uuid> \
* --query 'P1 | checkout-api error rate above SLO'
*
* # Also download session-report.md when the workflow finishes:
* npx tsx webhook-wait-session-step.ts … --download-report
*/
import { writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { parseArgs } from "node:util";
import { StackgenConfig, StackgenError } from "stackgen-sdk";
async function requestJson(
url: string,
token: string,
init: RequestInit = {},
): Promise<Record<string, unknown>> {
const response = await fetch(url, {
...init,
headers: {
Authorization: `Bearer ${token}`,
...(init.headers ?? {}),
},
});
if (!response.ok) {
throw new StackgenError(`HTTP ${response.status} for ${url}`);
}
const data = (await response.json()) as unknown;
if (!data || typeof data !== "object" || Array.isArray(data)) {
throw new StackgenError(`unexpected JSON payload from ${url}`);
}
return data as Record<string, unknown>;
}
function text(value: unknown): string {
if (value === null || value === undefined) {
return "";
}
return String(value).trim();
}
function orgQuery(orgId: string): string {
return orgId ? `?${new URLSearchParams({ orgId }).toString()}` : "";
}
async function waitForArtifact(
config: StackgenConfig,
sessionId: string,
artifactName: string,
): Promise<void> {
const deadline = Date.now() + config.timeoutSeconds * 1000;
const url =
`${config.aidenUrl()}/api/v1/sessions/${sessionId}/artifacts` + orgQuery(config.orgId);
const want = artifactName.toLowerCase();
while (Date.now() < deadline) {
const payload = await requestJson(url, config.apiToken);
const items = Array.isArray(payload)
? payload
: Array.isArray(payload.items)
? payload.items
: [];
if (
items.some(
(item) =>
item &&
typeof item === "object" &&
text((item as Record<string, unknown>).name).toLowerCase() === want,
)
) {
return;
}
await new Promise((resolveSleep) => setTimeout(resolveSleep, config.pollIntervalSeconds * 1000));
}
throw new StackgenError(`timed out waiting for artifact ${artifactName}`);
}
async function downloadArtifact(
config: StackgenConfig,
sessionId: string,
artifactName: string,
outputPath: string,
): Promise<string> {
const encoded = encodeURIComponent(artifactName);
const url =
`${config.aidenUrl()}/api/v1/sessions/${sessionId}/artifacts/${encoded}/download` +
orgQuery(config.orgId);
const response = await fetch(url, {
headers: { Authorization: `Bearer ${config.apiToken}` },
});
if (!response.ok) {
throw new StackgenError(`HTTP ${response.status} downloading ${artifactName}`);
}
const dest = resolve(outputPath);
await writeFile(dest, Buffer.from(await response.arrayBuffer()));
return dest;
}
async function main(): Promise<void> {
const { values } = parseArgs({
options: {
"base-url": { type: "string" },
"api-token": { type: "string" },
"org-id": { type: "string" },
"webhook-token": { type: "string" },
query: { type: "string", default: "Test webhook from stackgen-sdk example" },
"json-body": { type: "boolean", default: false },
"download-report": { type: "boolean", default: false },
"artifact-name": { type: "string", default: "session-report.md" },
"output-path": { type: "string", default: "session-report.md" },
"timeout-seconds": { type: "string", default: "1800" },
"poll-interval-seconds": { type: "string", default: "5" },
},
});
const webhookToken = values["webhook-token"] || process.env.WEBHOOK_TOKEN || "";
if (!values["base-url"] || !values["api-token"] || !values["org-id"]) {
throw new Error("require --base-url, --api-token, --org-id");
}
if (!webhookToken) {
throw new Error("set --webhook-token or WEBHOOK_TOKEN");
}
const config = new StackgenConfig({
baseUrl: values["base-url"],
apiToken: values["api-token"],
webhookToken,
orgId: values["org-id"],
artifactName: values["artifact-name"],
outputPath: values["output-path"],
timeoutSeconds: Number(values["timeout-seconds"]),
pollIntervalSeconds: Number(values["poll-interval-seconds"]),
});
const payload = values["json-body"]
? JSON.stringify({ message: values.query, query: values.query })
: values.query!;
const contentType = values["json-body"] ? "application/json" : "text/plain";
const url = `${config.aidenUrl()}/api/v1/webhooks/trigger${orgQuery(config.orgId)}`;
console.error(`POST ${url} content_type=${contentType}`);
const trigger = await requestJson(url, config.webhookToken!, {
method: "POST",
headers: { "Content-Type": contentType },
body: payload,
});
console.log(JSON.stringify(trigger, null, 2));
const sessionId = text(trigger.session_id);
if (!sessionId) {
throw new StackgenError("trigger response missing session_id");
}
console.error(`sessionId=${sessionId}`);
if (values["download-report"]) {
await waitForArtifact(config, sessionId, values["artifact-name"]!);
const path = await downloadArtifact(
config,
sessionId,
values["artifact-name"]!,
values["output-path"]!,
);
console.error(`outputPath=${path}`);
}
}
main().catch((err) => {
console.error(err instanceof Error ? err.message : err);
process.exit(1);
});Related: Webhook → session → artifact — adds artifact wait and download.