Skip to main content

Automatically move task documents to "Completed" when the parent task is closed

This example shows how to automatically move all documents related to a task into a common Completed folder when the task is closed. In a typical setup:

  • each task in an external system (task tracker, CRM, etc.) has its own DocSpace folder for documents,
  • when the task status becomes closed, you move all files from that task folder into a global Completed folder.

The external system is abstract in this sample: the script receives a list of tasks containing:

  • id — task identifier,
  • status — task status (open, in_progress, closed),
  • docsFolderId — DocSpace folder ID where task documents are stored.

Before you start

  1. Replace https://yourportal.onlyoffice.com and YOUR_API_KEY with your actual DocSpace portal URL and API key. Ensure you have the necessary data and permissions to perform these operations.
  2. Before you can make requests to the API, you need to authenticate. Check out the Personal access tokens page to learn how to obtain and use access tokens.
Full example
// Automatically move task documents to "Completed" when a task is closed.

const API_HOST = process.env.DOCSPACE_API_HOST; // Set DOCSPACE_API_HOST in env (recommended). For quick tests you can temporarily paste your portal URL here.
const API_KEY = process.env.DOCSPACE_API_KEY; // Set DOCSPACE_API_KEY in env (recommended). For quick tests you can temporarily paste token here.

// Global folder where all completed task documents will be moved
const COMPLETED_FOLDER_ID = 463996; // replace

const HEADERS: Record<string, string> = {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
};

type Task = {
id: string;
status: "open" | "in_progress" | "closed";
docsFolderId: number | null;
};

async function docspaceRequest(path: string, method: string, jsonBody?: any) {
const url = `${API_HOST}${path}`;

let res: Response;
try {
res = await fetch(url, {
method,
headers: HEADERS,
body: jsonBody ? JSON.stringify(jsonBody) : undefined,
});
} catch (e: any) {
console.error(`[ERROR] DocSpace request error: ${e?.message || e}`);
return null;
}

if (!res.ok) {
const text = await res.text().catch(() => "");
console.error(`[ERROR] DocSpace request failed: ${method} ${url}`);
console.error(`[ERROR] Status: ${res.status}, Message: ${text}`);
return null;
}

try {
return await res.json();
} catch {
return null;
}
}

// Step 1: Load tasks from external system (placeholder)
async function fetchTasksFromExternalSystem(): Promise<Task[]> {
return [
{ id: "TASK-101", status: "open", docsFolderId: 539564 },
{ id: "TASK-102", status: "closed", docsFolderId: 341029 },
{ id: "TASK-103", status: "closed", docsFolderId: 694513 },
];
}

function extractFilesFromFolderResponse(data: any): any[] {
// Expected common shape:
// { response: { files: [...], folders: [...] } }
const node = data && typeof data === "object" ? (data.response ?? null) : null;
if (!node || typeof node !== "object") return [];

const files = Array.isArray(node.files) ? node.files : [];
return files.filter((x) => x && typeof x === "object");
}

// Step 2: Read task folder contents (files only)
async function listFilesInFolder(folderId: number): Promise<any[]> {
const data = await docspaceRequest(`/api/2.0/files/${folderId}`, "GET");
if (!data) return [];
return extractFilesFromFolderResponse(data);
}

function isValidFileItem(item: any): boolean {
// We read from response.files, but still keep basic validation
return Boolean(item?.id && item?.title);
}

// Step 3: Move all files into the Completed folder
async function moveFilesToCompleted(fileIds: number[]): Promise<boolean> {
if (!fileIds.length) return true;

const payload = {
fileIds,
destFolderId: COMPLETED_FOLDER_ID,
deleteAfter: true, // move (not copy)
content: true,
toFillOut: false,
};

const data = await docspaceRequest("/api/2.0/files/fileops/move", "PUT", payload);
return data !== null;
}

async function processClosedTask(task: Task) {
if (!task.docsFolderId) {
console.log(`[INFO] Task ${task.id} has no docsFolderId. Skipping.`);
return;
}

console.log(`[INFO] Task ${task.id} is closed. Scanning folder ${task.docsFolderId}...`);

const files = await listFilesInFolder(task.docsFolderId);
const fileIds = files
.filter(isValidFileItem)
.map((f) => Number(f.id))
.filter((n) => Number.isFinite(n));

if (!fileIds.length) {
console.log(`[INFO] No files found in folder ${task.docsFolderId}.`);
return;
}

console.log(`[INFO] Moving ${fileIds.length} file(s) to Completed folder ${COMPLETED_FOLDER_ID}...`);
const ok = await moveFilesToCompleted(fileIds);

if (ok) console.log(`[OK] Task ${task.id}: files moved successfully.`);
else console.error(`[ERROR] Task ${task.id}: move failed.`);
}

// Main workflow
async function run() {
console.log("[INFO] Loading tasks...");
const tasks = await fetchTasksFromExternalSystem();

const closedTasks = tasks.filter((t) => t.status === "closed" && t.docsFolderId);
if (!closedTasks.length) {
console.log("[INFO] No closed tasks found.");
return;
}

console.log(`[INFO] Found ${closedTasks.length} closed task(s). Processing...`);

for (const t of closedTasks) {
await processClosedTask(t);
}

console.log("[INFO] Completed task document processing finished.");
}

run().catch((e: any) => {
console.error("[ERROR] Script failed:", e?.message || e);
process.exitCode = 1;
});

Step 1: Identify closed tasks with linked DocSpace folders

Your external system provides a task list where each task includes docsFolderId — the DocSpace folder that stores its documents.

The script keeps only tasks where:

  • status is closed
  • docsFolderId is not empty
async function fetchTasksFromExternalSystem(): Promise<Task[]> {
return [
{ id: "TASK-101", status: "open", docsFolderId: 539564 },
{ id: "TASK-102", status: "closed", docsFolderId: 341029 },
{ id: "TASK-103", status: "closed", docsFolderId: 694513 },
];
}

function extractFilesFromFolderResponse(data: any): any[] {
// Expected common shape:
// { response: { files: [...], folders: [...] } }
const node = data && typeof data === "object" ? (data.response ?? null) : null;
if (!node || typeof node !== "object") return [];

const files = Array.isArray(node.files) ? node.files : [];
return files.filter((x) => x && typeof x === "object");
}

Step 2: Read file items from the task document folder

For each closed task, the script loads the folder contents using GET /api/2.0/files/:docsFolderId From the response, it uses response.files and ignores folders (because only documents need to be moved).

async function listFilesInFolder(folderId: number): Promise<any[]> {
const data = await docspaceRequest(`/api/2.0/files/${folderId}`, "GET");
if (!data) return [];
return extractFilesFromFolderResponse(data);
}

function isValidFileItem(item: any): boolean {
// We read from response.files, but still keep basic validation
return Boolean(item?.id && item?.title);
}

Step 3: Move all task documents to the global Completed folder

If the task folder contains files, the script moves them into the common Completed location using PUT /api/2.0/files/fileops/move with body.

As a result:

  • the Completed folder becomes the single place where finished task documents live,
  • each task document folder becomes empty (or “clean”) after the task is closed.
async function moveFilesToCompleted(fileIds: number[]): Promise<boolean> {
if (!fileIds.length) return true;

const payload = {
fileIds,
destFolderId: COMPLETED_FOLDER_ID,
deleteAfter: true, // move (not copy)
content: true,
toFillOut: false,
};

const data = await docspaceRequest("/api/2.0/files/fileops/move", "PUT", payload);
return data !== null;
}