Skip to content

Approvals

Some applications publish a document only after someone approved it. The File Service keeps that lifecycle on the file itself, as status, and enforces who may move it. This guide is for backend developers and administrators of document applications; an application that does not use approvals leaves every file at draft and can ignore it.

Action on the item: approve

Set approvals_enabled: true in the application’s settings. The console then shows the status column and the review actions. Then decide who approves: map files.approve onto a role on the Permissions page for the whole application, or grant the approve action with an entry on the folders that need review.

From To Needs
draft in_review, archived edit
draft approved, rejected approve
in_review approved, rejected, draft approve
in_review archived edit
approved draft (reopen) approve
approved archived edit
rejected draft, in_review, archived edit
archived draft edit

Any other move answers status_locked. Change the status with PATCH P/files/:file_id and { "status": "…" }, or set it on the upload ticket.

  • A file in_review or approved refuses new versions and metadata changes from people without approve (status_locked).
  • An archived file refuses changes until it is moved back to draft; it can still be trashed.
  • A new version of a file that is not a draft reopens it as draft, so an approved document is never silently replaced.
approvals.ts
import { FilesConflictError, FilesPreconditionFailedError } from '@central-code-station/ccs-files-client';
import { files } from './files-client';
// With approvals on (settings.approvals_enabled), a document moves draft -> in_review -> approved or rejected.
// acting_user_id names the person your backend acts for, on the audit row.
export async function submitForReview(fileId: string, userId: string) {
const file = await files.files.get(fileId);
return files.files.update(fileId, { status: 'in_review', acting_user_id: userId }, { ifMatch: file.revision });
}
export async function decide(fileId: string, reviewerId: string, approve: boolean) {
// Your backend may do anything; check that the reviewer holds `approve` on this file before acting for them.
const { actions } = await files.files.effectivePermissions(fileId, reviewerId);
if (!actions.includes('approve')) throw new Error('not a reviewer of this file');
const file = await files.files.get(fileId);
try {
return await files.files.update(fileId, { status: approve ? 'approved' : 'rejected', acting_user_id: reviewerId }, { ifMatch: file.revision });
} catch (error) {
if (error instanceof FilesPreconditionFailedError) throw new Error('the file changed meanwhile: reload and decide again');
if (error instanceof FilesConflictError) throw new Error(`refused: ${error.code}`); // status_locked, file_locked
throw error;
}
}

Your machine client may set any status. The sample checks the reviewer’s own rights first with GET P/files/:file_id/effective-permissions, and sends the revision as If-Match, so two reviewers deciding at once cannot overwrite each other (revision_mismatch).

Every change publishes file.status.changed with from, to and by_user_id. The owner receives it; on in_review the users who may approve the file receive it too, with a push notification when they are offline, and the owner gets a push on approved and rejected. Every change is in the audit log as file.status.changed. Search and listings filter by status.