{"record":{"id":"f141f81489ea1a50","repo":"hcengineering/platform","slug":"archiving-allowed-only-for-active-workspaces","errorCode":null,"errorMessage":"Archiving allowed only for active workspaces","messagePattern":"Archiving allowed only for active workspaces","errorType":"exception","errorClass":"PlatformError","httpStatus":null,"severity":"error","filePath":"server/account/src/serviceOperations.ts","lineNumber":169,"sourceCode":"    const update: Partial<WorkspaceStatus> = {}\n    switch (event) {\n      case 'reset-attempts':\n        update.processingAttempts = 0\n        update.lastProcessingTime = Date.now() - processingTimeoutMs // To not wait for next step\n        break\n      case 'delete':\n        if (workspace.status.mode !== 'active') {\n          throw new PlatformError(unknownError('Delete allowed only for active workspaces'))\n        }\n\n        update.mode = 'pending-deletion'\n        update.processingAttempts = 0\n        update.processingProgress = 0\n        update.lastProcessingTime = Date.now() - processingTimeoutMs // To not wait for next step\n        break\n      case 'archive':\n        if (!isActiveMode(workspace.status.mode)) {\n          throw new PlatformError(unknownError('Archiving allowed only for active workspaces'))\n        }\n\n        update.mode = 'archiving-pending-backup'\n        update.processingAttempts = 0\n        update.processingProgress = 0\n        update.lastProcessingTime = Date.now() - processingTimeoutMs // To not wait for next step\n        break\n      case 'unarchive':\n        if (event === 'unarchive') {\n          if (workspace.status.mode !== 'archived') {\n            throw new PlatformError(unknownError('Unarchive allowed only for archived workspaces'))\n          }\n        }\n\n        update.mode = 'pending-restore'\n        update.processingAttempts = 0\n        update.processingProgress = 0\n        update.lastProcessingTime = Date.now() - processingTimeoutMs // To not wait for next step","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/server/account/src/serviceOperations.ts#L151-L187","documentation":"The 'archive' branch of performWorkspaceOperation only allows archiving workspaces whose status.mode satisfies isActiveMode. Archiving is a lifecycle transition from an active state to 'archiving-pending-backup'; issuing it against a workspace in any other mode (archived, pending-deletion, suspended, transitional) throws unknownError('Archiving allowed only for active workspaces').","triggerScenarios":"Calling performWorkspaceOperation with event='archive' on a workspace whose mode is not active — most commonly an already-archived workspace, one already pending deletion, or one stuck in 'archiving-pending-backup' from a previous archive request.","commonSituations":"Retrying an archive request that already succeeded (workspace now archived or mid-backup); a scheduled job archiving a list of workspaces where some are already archived; UI showing stale status so the user archives twice; concurrent archive/delete operations racing on the same workspace.","solutions":["Check workspace.status.mode with isActiveMode before issuing event='archive'; skip workspaces already archived or pending-deletion.","If the workspace is already archived, the goal is achieved — no operation needed.","If it's stuck in 'archiving-pending-backup', wait for the backup worker or investigate the processing state (reset-attempts can unstick a stalled processor) before re-archiving.","Make archive requests idempotent client-side: fetch current status and deduplicate concurrent requests."],"exampleFix":"// before — archive unconditionally\nawait client.performWorkspaceOperation(ctx, adminToken, { workspaceId: wsId, event: 'archive', params: {} })\n// after — only archive active workspaces\nconst [ws] = await getWorkspaceInfo(ctx, adminToken, wsId)\nif (isActiveMode(ws.status.mode)) {\n  await client.performWorkspaceOperation(ctx, adminToken, { workspaceId: wsId, event: 'archive', params: {} })\n}","handlingStrategy":"validation","validationCode":"const [ws] = await getWorkspacesInfoWithStatusByIds(db, [workspaceId])\nif (!ws || !isActiveMode(ws.status.mode)) {\n  throw new Error(`Cannot archive workspace in mode '${ws?.status.mode}'; archive requires an active workspace`)\n}","typeGuard":"function isArchivable(ws: { status: { mode: string } } | undefined): ws is { status: { mode: 'active' | 'restoring' } } {\n  return ws !== undefined && isActiveMode(ws.status.mode)\n}","tryCatchPattern":"try {\n  await client.performWorkspaceOperation(ctx, token, { workspaceId, event: 'archive', params: {} })\n} catch (err) {\n  if (String(err?.message).includes('Archiving allowed only for active workspaces')) {\n    // re-fetch status; treat already-archived as success\n    return\n  }\n  throw err\n}","preventionTips":["Re-fetch workspace status right before archiving; never trust stale UI state.","Make archive flows idempotent — an already-archived workspace is a no-op, not an error.","Guard scheduled archive jobs to filter on isActiveMode only.","Serialize concurrent archive/delete requests per workspace to prevent races."],"tags":["state-machine","workspace-lifecycle","precondition-failed"],"backgroundTag":"invalid-resource-state","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}