Chat subscriptions
Your SRE app (or any installed catalog app) should not be a free-for-all across every Slack channel. A chat subscription is the routing rule: which workspace channels, which triggers (mention, alert bot), and which installed app receives the normalized event.
You create and manage those rules from the SDK the same way you already create webhooks and schedules. Provision them in a bootstrap job, flip them off for a noisy channel, or tear them down when a temporary war-room channel closes.
Aiden fills the POST path from the app’s discovery manifest
(chat_subscriptions[].endpoint_path). If the app declares more than one
app-target route, pass bundle_name (that route’s manifest name).
When you need this
- Stand up a new on-call channel.
#checkout-sevshould mention-route into your SRE app without clicking through the UI for every environment. - Silence without uninstalling. Toggle
enabled: falseduring a planned load test so alert bots do not open fifty threads. - Ephemeral war rooms. Create a subscription for a temporary channel when an incident starts; delete it when the room archives.
- Multi-app orgs. Payroll bots and SRE bots share Slack. Subscriptions keep each app’s mentions and alert bots on their own app.
- Infra as code for chat. Treat channel routing like webhooks: create in CI, assert it exists in a smoke test, delete on teardown.
If you only use the product UI once and never change routing, you may not need the API. The moment routing is part of how you ship environments, you want it in code.
How to run
export STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
pip install stackgen-sdk==0.3.0
python examples/automation/chat-subscriptions.py --app-name sreexport STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
npm install stackgen-sdk@0.3.0 tsx
npx tsx examples/automation/chat-subscriptions.ts --app-name sreexport STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
go run ./examples/go/automation/chat-subscriptions.go --app-name sreOptional: --channel-id C0123456789 to scope the selector to one channel.
Minimal
from stackgen import StackgenClient, StackgenConfig
client = StackgenClient(
StackgenConfig(
base_url="https://app.stackgen.com",
api_token="stackgen_…",
workspace_id="<workspace-uuid>",
)
)
# Point @mentions in selected channels at your installed SRE app.
# Aiden fills endpoint_path from the app discovery manifest.
sub = client.aiden.create_chat_subscription({
"name": "checkout-sev-mentions",
"app_name": "sre",
"target_type": "app",
"target_name": "sre",
"channel_selector": {"channel_ids": ["C0123456789"]},
"trigger_types": ["mention"],
"enabled": True,
})
# Quiet the route without deleting it (load test window, maintenance).
client.aiden.toggle_chat_subscription(sub["id"], {"enabled": False})
# War room closed.
client.aiden.delete_chat_subscription(sub["id"])import { StackgenClient, StackgenConfig } from "stackgen-sdk";
const client = new StackgenClient(
new StackgenConfig({
baseUrl: "https://app.stackgen.com",
apiToken: "stackgen_…",
workspaceId: "<workspace-uuid>",
}),
);
const sub = await client.aiden.createChatSubscription({
name: "checkout-sev-mentions",
app_name: "sre",
target_type: "app",
target_name: "sre",
channel_selector: { channel_ids: ["C0123456789"] },
trigger_types: ["mention"],
enabled: true,
});
await client.aiden.toggleChatSubscription(String(sub.id), { enabled: false });
await client.aiden.deleteChatSubscription(String(sub.id));sub, err := client.Aiden.CreateChatSubscription(ctx, map[string]any{
"name": "checkout-sev-mentions",
"app_name": "sre",
"target_type": "app",
"target_name": "sre",
"channel_selector": map[string]any{
"channel_ids": []string{"C0123456789"},
},
"trigger_types": []string{"mention"},
"enabled": true,
})
if err != nil { return err }
id := fmt.Sprint(sub["id"])
if _, err = client.Aiden.ToggleChatSubscription(ctx, id, map[string]any{"enabled": false}); err != nil {
return err
}
_, err = client.Aiden.DeleteChatSubscription(ctx, id)
return errComplete scripts
- Python:
examples/automation/chat-subscriptions.py - TypeScript:
examples/automation/chat-subscriptions.ts - Go:
examples/go/automation/chat-subscriptions.go
#!/usr/bin/env python3
"""Provision Slack/Teams routing for an installed app from code.
Chat subscriptions decide which channels and triggers land on which app.
Aiden resolves the app POST path from the installed app's discovery
manifest. Use them to stand up an on-call channel, mute a route during a
load test, or tear down a war-room subscription when the room closes.
Requires STACKGEN_URL, STACKGEN_TOKEN, and STACKGEN_WORKSPACE_ID.
The default target is an installed app. Pass --target-type agent --agent to
route the same subscription lifecycle at an agent instead.
Usage:
export STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
python examples/automation/chat-subscriptions.py --app-name sre
python examples/automation/chat-subscriptions.py --target-type agent --agent my-oncall-agent
"""
from __future__ import annotations
import argparse
import os
import sys
import uuid
from stackgen import StackgenClient, StackgenConfig
def env(name: str, *fallbacks: str) -> str:
for key in (name, *fallbacks):
value = (os.environ.get(key) or "").strip()
if value:
return value
raise SystemExit(f"missing env {name}")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--app-name",
default="",
help="Installed catalog app name. Required when --target-type is app.",
)
parser.add_argument(
"--target-type",
choices=("app", "agent"),
default="app",
help="Route matching chat events to an installed app or an agent",
)
parser.add_argument(
"--agent",
default="",
help="Agent name. Required when --target-type is agent.",
)
parser.add_argument(
"--channel-id",
default="",
help="Optional Slack/Teams channel id to constrain the selector",
)
parser.add_argument(
"--bundle-name",
default="",
help="Optional AppChatSubscriptionSpec.name when the app declares multiple routes",
)
args = parser.parse_args()
if args.target_type == "agent":
if not args.agent:
parser.error("--agent is required when --target-type agent")
target_name = args.agent
else:
if not args.app_name:
parser.error("--app-name is required when --target-type app")
target_name = args.app_name
client = StackgenClient(
StackgenConfig(
base_url=env("STACKGEN_URL"),
api_token=env("STACKGEN_TOKEN"),
workspace_id=env("STACKGEN_WORKSPACE_ID", "STACKGEN_PROJECT"),
)
)
suffix = uuid.uuid4().hex[:8]
body: dict = {
"name": f"sdk-chat-sub-{suffix}",
"target_type": args.target_type,
"target_name": target_name,
# channel_selector is required; tag matches Active-channels routing.
"channel_selector": (
{"channel_ids": [args.channel_id]}
if args.channel_id
else {"tag": "sdk-ci"}
),
"trigger_types": ["mention"],
"enabled": True,
}
if args.app_name:
body["app_name"] = args.app_name
if args.bundle_name:
body["bundle_name"] = args.bundle_name
created = client.aiden.create_chat_subscription(body)
sub_id = str(created.get("id") or "")
if not sub_id:
raise SystemExit(f"create_chat_subscription missing id: {created}")
print(f"created subscription id={sub_id} target_type={args.target_type}")
try:
got = client.aiden.get_chat_subscription(sub_id)
print(f"get enabled={got.get('enabled')} endpoint={got.get('endpoint_path')}")
list_kwargs = {"app_name": args.app_name} if args.app_name else {}
listed = client.aiden.list_chat_subscriptions(**list_kwargs)
print(f"list count={len(listed)}")
toggled = client.aiden.toggle_chat_subscription(sub_id, {"enabled": False})
print(f"toggled enabled={toggled.get('enabled')}")
finally:
try:
client.aiden.delete_chat_subscription(sub_id)
print(f"deleted subscription id={sub_id}")
except Exception as exc: # noqa: BLE001
print(f"delete subscription id={sub_id} failed: {exc}", file=sys.stderr)
raise
return 0
if __name__ == "__main__":
raise SystemExit(main())/**
* Provision Slack/Teams routing for an installed app from code.
*
* Chat subscriptions decide which channels and triggers land on which app.
* Aiden resolves the app POST path from the installed app's discovery
* manifest. Stand up an on-call channel, mute a route during a load test, or
* tear down a war-room subscription when the room closes.
*
* Usage:
* export STACKGEN_URL=https://app.stackgen.com
* export STACKGEN_TOKEN=stackgen_…
* export STACKGEN_WORKSPACE_ID=<workspace-uuid>
* npx tsx examples/automation/chat-subscriptions.ts --app-name sre
* npx tsx examples/automation/chat-subscriptions.ts --target-type agent --agent my-oncall-agent
*/
import { parseArgs } from "node:util";
import { randomUUID } from "node:crypto";
import { StackgenClient, StackgenConfig } from "stackgen-sdk";
function requireEnv(name: string, ...fallbacks: string[]): string {
for (const key of [name, ...fallbacks]) {
const value = (process.env[key] || "").trim();
if (value) {
return value;
}
}
throw new Error(`missing env ${name}`);
}
async function main(): Promise<void> {
const { values } = parseArgs({
options: {
"app-name": { type: "string", default: "" },
"target-type": { type: "string", default: "app" },
agent: { type: "string", default: "" },
"channel-id": { type: "string", default: "" },
"bundle-name": { type: "string", default: "" },
},
});
const targetType = values["target-type"] || "app";
if (targetType !== "app" && targetType !== "agent") {
throw new Error("--target-type must be app or agent");
}
if (targetType === "agent" && !values.agent) {
throw new Error("require --agent when --target-type agent");
}
if (targetType === "app" && !values["app-name"]) {
throw new Error("require --app-name when --target-type app");
}
const targetName = targetType === "agent" ? values.agent! : values["app-name"]!;
const client = new StackgenClient(
new StackgenConfig({
baseUrl: requireEnv("STACKGEN_URL"),
apiToken: requireEnv("STACKGEN_TOKEN"),
workspaceId: requireEnv("STACKGEN_WORKSPACE_ID", "STACKGEN_PROJECT"),
}),
);
const suffix = randomUUID().slice(0, 8);
// channel_selector is required; tag matches Active-channels routing.
const channelSelector = values["channel-id"]
? { channel_ids: [values["channel-id"]] }
: { tag: "sdk-ci" };
const created = await client.aiden.createChatSubscription({
name: `sdk-chat-sub-${suffix}`,
...(values["app-name"] ? { app_name: values["app-name"] } : {}),
target_type: targetType,
target_name: targetName,
...(values["bundle-name"] ? { bundle_name: values["bundle-name"] } : {}),
channel_selector: channelSelector,
trigger_types: ["mention"],
enabled: true,
});
const subId = String(created.id ?? "");
if (!subId) {
throw new Error(`createChatSubscription missing id: ${JSON.stringify(created)}`);
}
console.log(`created subscription id=${subId}`);
try {
const got = await client.aiden.getChatSubscription(subId);
console.log(`get enabled=${got.enabled} endpoint=${got.endpoint_path}`);
const listed = await client.aiden.listChatSubscriptions(
values["app-name"] ? { app_name: values["app-name"] } : {},
);
console.log(`list count=${listed.length}`);
const toggled = await client.aiden.toggleChatSubscription(subId, {
enabled: false,
});
console.log(`toggled enabled=${toggled.enabled}`);
} finally {
try {
await client.aiden.deleteChatSubscription(subId);
console.log(`deleted subscription id=${subId}`);
} catch (err) {
console.error(`delete subscription id=${subId} failed:`, err);
throw err;
}
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});// Create a chat subscription, mute it, then delete it.
//
// Documented at https://appcd-dev.github.io/stackgen-sdk/code-samples/chat-subscriptions/
//
// go run ./examples/go/automation/chat-subscriptions.go --app-name sre
// go run ./examples/go/automation/chat-subscriptions.go --target-type agent --agent my-oncall-agent
package main
import (
"context"
"flag"
"fmt"
"os"
"strings"
stackgen "github.com/appcd-dev/stackgen-sdk/clients/go"
)
// envOr returns the first non-empty environment variable among keys.
func envOr(keys ...string) string {
for _, key := range keys {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
}
return ""
}
func main() {
appName := flag.String("app-name", "", "Installed catalog app name")
targetType := flag.String("target-type", "app", "app or agent")
agent := flag.String("agent", "", "Agent name when --target-type is agent")
channelID := flag.String("channel-id", "", "Optional channel id")
flag.Parse()
if *targetType != "app" && *targetType != "agent" {
fmt.Fprintln(os.Stderr, "--target-type must be app or agent")
os.Exit(2)
}
targetName := *appName
if *targetType == "agent" {
if strings.TrimSpace(*agent) == "" {
fmt.Fprintln(os.Stderr, "--agent is required when --target-type agent")
os.Exit(2)
}
targetName = *agent
} else if strings.TrimSpace(*appName) == "" {
fmt.Fprintln(os.Stderr, "--app-name is required when --target-type app")
os.Exit(2)
}
base, token, org := envOr("STACKGEN_URL"), envOr("STACKGEN_TOKEN"), envOr("STACKGEN_WORKSPACE_ID", "STACKGEN_PROJECT")
if base == "" || token == "" || org == "" {
fmt.Fprintln(os.Stderr, "set STACKGEN_URL, STACKGEN_TOKEN, and STACKGEN_WORKSPACE_ID")
os.Exit(2)
}
client, err := stackgen.NewClient(stackgen.Config{BaseURL: base, APIToken: token, WorkspaceID: org})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
body := map[string]any{
"name": "checkout-sev-mentions",
"app_name": targetName,
"target_type": *targetType,
"target_name": targetName,
"trigger_types": []string{"mention"},
"enabled": true,
}
if *channelID != "" {
body["channel_selector"] = map[string]any{"channel_ids": []string{*channelID}}
}
ctx := context.Background()
sub, err := client.Aiden.CreateChatSubscription(ctx, body)
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
id, _ := sub["id"].(string)
fmt.Printf("created subscription id=%s\n", id)
if _, err := client.Aiden.ToggleChatSubscription(ctx, id, map[string]any{"enabled": false}); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
fmt.Println("toggled enabled=false")
if _, err := client.Aiden.DeleteChatSubscription(ctx, id); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
fmt.Println("deleted subscription")
}