{"record":{"id":"35d7f2c5f8d48e9b","repo":"multica-ai/multica","slug":"slack-this-slack-app-is-already-connected-to-a-di","errorCode":null,"errorMessage":"slack: this Slack app is already connected to a different Multica workspace","messagePattern":"slack: this Slack app is already connected to a different Multica workspace","errorType":"http","errorClass":"ErrTeamOwnedByAnotherWorkspace","httpStatus":409,"severity":"error","filePath":"server/internal/integrations/slack/install.go","lineNumber":35,"sourceCode":")\n\n// This file is the Slack install backend (MUL-3666). Slack uses the\n// bring-your-own-app (BYO) model: the workspace admin creates their own Slack\n// app, installs it to their Slack workspace, and pastes its bot token (xoxb-) +\n// app-level token (xapp-) into Multica (the paste path lives in byo_install.go).\n// The InstallService owns the at-rest encryption of those tokens — so no caller\n// can write a channel_installation with a plaintext token — plus the shared\n// persistInstall transaction and the list / get / revoke management surface.\n\nvar (\n\t// ErrInstallationNotFound surfaces \"no row matches in this workspace\".\n\tErrInstallationNotFound = errors.New(\"slack installation not found\")\n\t// ErrTeamOwnedByAnotherWorkspace is returned when the pasted Slack app is\n\t// already connected to a live owner in a DIFFERENT Multica workspace — it\n\t// would collide with the (channel_type, app_id) routing index. A Slack app is\n\t// one bot identity and maps to one agent; reusing it here requires\n\t// disconnecting it in the other workspace first.\n\tErrTeamOwnedByAnotherWorkspace = errors.New(\"slack: this Slack app is already connected to a different Multica workspace\")\n\t// ErrTeamOwnedBySameWorkspace is returned when the app is already connected to\n\t// a DIFFERENT (live, non-archived) agent in the SAME workspace. The old\n\t// catch-all wrongly blamed \"another workspace\"; naming the same-workspace case\n\t// points the user at the Disconnect they can actually reach (#4810).\n\tErrTeamOwnedBySameWorkspace = errors.New(\"slack: this Slack app is already connected to another agent in this workspace\")\n\t// ErrTeamOwnedByArchivedAgent is returned when the app's owning agent is\n\t// archived (and so still holds the bot, since archiving is reversible). The\n\t// user recovers by restoring that agent or disconnecting its bot.\n\tErrTeamOwnedByArchivedAgent = errors.New(\"slack: this Slack app is connected to an archived agent in this workspace\")\n)\n\n// installQueries is the slice of generated queries InstallService needs. WithTx\n// returns the same interface bound to a transaction so persistInstall runs its\n// upsert atomically (and so tests can inject a fake without a real DB).\ntype installQueries interface {\n\tWithTx(tx pgx.Tx) installQueries\n\tUpsertChannelInstallation(ctx context.Context, arg db.UpsertChannelInstallationParams) (db.ChannelInstallation, error)\n\tReclaimDeadChannelInstallationByAppID(ctx context.Context, arg db.ReclaimDeadChannelInstallationByAppIDParams) (pgtype.UUID, error)","sourceCodeStart":17,"sourceCodeEnd":53,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/integrations/slack/install.go#L17-L53","documentation":"Slack install ownership error: the pasted Slack app (identified by app id) is already connected to a live owner in a DIFFERENT Multica workspace, which would collide with the (channel_type, app_id) routing index. A Slack app is one bot identity mapped to one agent, so it must be disconnected in the other workspace first.","triggerScenarios":"Calling RegisterBYO/persistInstall while a live (non-revoked) channel_installation for the same app id exists under another workspace's agent — e.g. the same Slack app was previously wired into a second Multica workspace or a leftover instance.","commonSituations":"Trialing in a sandbox workspace then moving to production without disconnecting; two teams independently connecting the same Slack app; dev/staging/prod Multica instances sharing one Slack app.","solutions":["In the OTHER Multica workspace that owns the app, run Disconnect on that installation to release it.","If the other workspace is unreachable (org split, deleted tenant), have an operator revoke/remove the owning channel_installation row directly.","Alternatively connect a DIFFERENT Slack app (new xoxb-/xapp- pair) to this workspace instead of sharing one."],"exampleFix":"// before\n_, err := installSvc.Register(ctx, params)\nif err != nil {\n\treturn err // surfaces raw to admin\n}\n\n// after\n_, err := installSvc.Register(ctx, params)\nswitch {\ncase errors.Is(err, slack.ErrTeamOwnedByAnotherWorkspace):\n\trespond(w, 409, \"disconnect this Slack app in its other workspace first\")\ncase errors.Is(err, slack.ErrTeamOwnedBySameWorkspace):\n\trespond(w, 409, \"disconnect it from the other agent in this workspace (Agents > Disconnect)\")\n}","handlingStrategy":"try-catch","validationCode":"owner, err := queries.GetChannelInstallationOwnerByAppID(ctx, \"slack\", appID)\nif err == nil && owner.WorkspaceID != thisWorkspace {\n\treturn respondConflict(w, \"disconnect this app in its other workspace first\")\n}\n_ = installSvc.Register(ctx, params)","typeGuard":null,"tryCatchPattern":"_, err := installSvc.Register(ctx, params)\nif err != nil {\n\tif errors.Is(err, slack.ErrTeamOwnedByAnotherWorkspace) {\n\t\treturn respondConflict(w, \"owned by another workspace — disconnect it there first\")\n\t}\n\treturn err\n}","preventionTips":["Use one Slack app per Multica workspace; never share apps across instances.","Always run Disconnect before decommissioning or migrating a workspace.","Document the (channel_type, app_id) uniqueness rule for admins connecting apps."],"tags":["slack","byo","conflict","routing-index","integration","go"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}