← Hand-off Files
API

Run Hand-off Files from your own code

The one metered task, handover, is available over HTTP. It takes the Google Drive files of someone leaving as the page has read them - what the manager knows, who is leaving and when, the manager and the counts, one numbered line per folder (f1, f2 ...) with its files, how many are stale, the last edit, how many are shared outside and with whom, and the inside collaborators with how many files each is on, and the smaller folders past the list counted - and returns one action per listed folder (review, archive, transfer, manager) with who receives it and a reason, a verdict (ready or blocked) and a note.

The division of labour first: the page reads the file list, keeps the files the person leaving owns, groups them by folder, sorts every collaborator into inside or outside, picks the colleague on the most of each folder's files and marks a file stale after 365 days with no edit before the leaving date; the run decides what happens to each listed folder and who receives it, naming folders only by id so the page can tie each action to real files. The page reads up to 20000 lines and lists up to 20 folders; the rest are counted and get no action. After the reply it keeps one known action per listed folder, refuses a transfer to someone who does not work on the folder, checks each action and receiver against its own rule and the verdict against the sharing, and warns when a sentence quotes a number or date the list does not have, names a folder by its id, or calls anyone he or she. Nothing is moved in Drive - not by the page and not by the run; the plan is a list for you to act on. Your code can do the same with the engine files the page loads (see "Build the input yourself").

Base URL and the response shape

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }

The token is minted for this app (the guest call takes {"slug":"hand-off-files"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field (task, goal, people, folders, others) is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: the handover run is metered and needs a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field, the input and the rules

Every run sends task: handover (the one task is handover); a missing or different one is still answered as handover. All fields are strings. What each holds, in the words of the system prompt, with the first sample's value (its first line where it has several):

FieldRequiredWhat it holds
taskyesAlways handover. If it is missing or different, still answer this task and set "task": "handover".
goalyesWhat the manager knows about the handover, in their words, or none. First sample: Ana leaves the marketing team on Nov 7; Priya runs the Q4 launch from here.
peopleyesWho is leaving and when, the manager, and how many files and folders. First sample: leaving: ana@northwind.example.com, files counted as of 2026-11-07; manager: lee@northwind.example.com; 11 files they own in 4 folders; stale means no edit in the 365 days before that date.
foldersyesOne line per folder: f1. Folder | N files | N stale or all stale | last edited date | nothing shared outside, or N files shared outside (who) | inside collaborators: address on N, ... or no inside collaborators. First sample: f1. Campaigns/Q4 | 4 files | 0 stale | last edited 2026-10-25 | nothing shared outside | inside collaborators: priya@northwind.example.com on 4, tom@northwind.example.com on 2 ...
othersyesSmaller folders past the list, counted, or none. They get no action. First sample: none.
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

How the page reads the file list (HandKit.readReport, called by HandKit.form): up to 20000 lines. JSON (text that starts with [ or {) is read as a Drive files list - a bare array, or an object with files, items or data, as gws drive files list or the Drive API returns it - where each file has a name (or title), a path (or folder), the owner in owners[0].emailAddress (or owner), the last edit in modifiedTime (or modified), and permissions: an entry of type anyone is a link share, any other non-owner entry with an emailAddress is a collaborator, and a domain entry counts as its domain; a sharedWith list is read too. A table (tab or comma separated) is found by its header row and needs a name column (name, file, file name, title, document, item); a folder column (a header holding path, folder, location, parent or directory), an owner column, a modified column (modified, updated, last edited, changed), any shared-with columns (shared, collaborator, people, access, editors, viewers, commenters, permission) and a link column (link, visibility, general access) are used when there are. Without a header, each line is split by |: the first part is the path and the file name, the first other part that reads as a date is the last edit, and the addresses in the rest are the collaborators, as in Campaigns/Q4 plan.docx | 2026-10-20 | priya@example.com. Dates are read as printed (2026-10-27, 2026-10-27T09:14:05Z, 10/27/2026, 27/10/2026); "anyone", "public" or "with the link" in a shared cell is a link share. A row with no file name is set aside and counted in the warning.

What the page works out, free and before any run. Ownership: only files owned by the person leaving (or with no owner given) are planned; with no address given, the most common owner is taken, and files owned by anyone else are set aside and counted. Inside or outside: a collaborator on the leaving person's domain (or a subdomain of it) is inside; a personal mailbox (Gmail, Outlook, iCloud and the like), any other domain and a link share count as shared outside. Stale: a file with no edit in the 365 days before the leaving date - or, with no date given, before the latest edit in the list. Folders: files are grouped by their first two folder levels ((no folder) when there is none); each folder's receiver is the inside collaborator on the most of its files, a tie going alphabetically. Folders are sorted by the number of files, then by name, and the first 20 get the ids f1 ... f20, each sent as one line (HandKit.folderLine: up to four outside names and the top three inside collaborators); the rest are counted in others and get no action. The page asks for a file list, the leaving person's address (or an owner it can infer) and the manager's address before it builds an input.

The model's rules

As the system prompt states them, in order:

  1. plan - one entry for each line in folders, in id order: folder (its id), action, to and why (one sentence, at most 20 words, using only that line, people and goal). Take the first action that applies:
    • review - the folder has files shared outside. Its sharing is checked before anything moves, because a transfer keeps every share. to is "".
    • archive - the folder is all stale. It goes to the manager to keep in an archive. to is the manager's address.
    • transfer - the folder has inside collaborators. It goes to the first one listed, who is on the most of its files. to is that address.
    • manager - anything else. to is the manager's address.
  2. verdict - blocked when any folder is review; ready otherwise.
  3. note - one sentence, at most 30 words: whether the handover can start, and the first thing to do - a review first, then the biggest transfer.

And the house rules:

The reply

One JSON object, in the shape the system prompt asks for:

{
  "task": "handover",
  "verdict": "ready | blocked",
  "plan": [ { "folder": "f1", "action": "review | archive | transfer | manager", "to": "an address or empty", "why": "one sentence" } ],
  "note": "one sentence"
}

The page reads the reply with ReplyKit.normalize (the verdict, the folder ids, the actions and the addresses matched in upper or lower letters; an entry with no folder id is dropped) and keeps what it trusts with ReplyKit.trusted(reply, built): an entry is set aside, with the reason, when it names a folder the page did not send, a folder that already has an action, an action the page does not know, or a transfer to someone who is not an inside collaborator on that folder; every listed folder with no action is listed as missing. t.acts maps each id to its action, t.plan to {action, to} and t.why to its reason. The files, the owners, the dates and the collaborators always come from the page's own reading, never from the reply.

ReplyKit.check(reply, built) lists, under "The page's check": a missing verdict, or one the sharing contradicts (HandKit.expectVerdict: blocked when any folder has a file shared outside); every entry set aside, and every listed folder with no action; an action that is not the one the rule gives (HandKit.expectAction(g): review a folder with files shared outside; archive one that is all stale; transfer one with inside collaborators; otherwise give it to the manager), or a receiver that is not the one the rule gives (HandKit.expectTo(g, form): nobody for a review, the receiver for a transfer, the manager otherwise); a number or date in a sentence that is not in the input; a sentence that names a folder id or an internal field or uses backticks; a sentence that calls anyone he, she, him or her; and words such as "again", "already", "always", "never" or "every" that the input never used. Any warning makes the status flagged; otherwise it is kept. The verdict on the page reads "The handover can start" or "Sharing needs a check first".

The exports: HandKit.toCSV(form, t.plan) writes one row per file, the folders past the list included, biggest folder first - with the folder id, the folder, the file, the last edit, whether it is stale, the inside collaborators, who outside can open it, the plan and who receives it, with CRLF line ends; ReplyKit.md and ReplyKit.json write the counts and every listed folder with its plan, receiver and reason; and the waffle (HandWaffle.draw, one square per file in a block per folder - in use, stale or shared outside - each block stamped with its action) saves as PNG or SVG.

Worked examples - every sample

These are the free samples on the page, one per paste shape: the form as filled in, how the page reads the list - the files the person leaving owns, the folders, the stale files, the outside shares and the inside collaborators - the exact input it sends, the reply that replays for free, and what the page does with it - the actions and receivers it trusts, folder by folder against its own rule, the verdict against the sharing, and its own check. Together they cover the verdicts blocked and ready.

A marketing manager (Drive JSON)

The form: who is leaving: ana@northwind.example.com; the manager: lee@northwind.example.com; the leaving date: 2026-11-07; anything to know: Ana leaves the marketing team on Nov 7; Priya runs the Q4 launch from here.

The file list as pasted:

{
 "files": [
  {
   "name": "Q4 launch plan.docx",
   "path": "Campaigns/Q4",
   "owners": [
    {
     "emailAddress": "ana@northwind.example.com"
    }
   ],
   "modifiedTime": "2026-10-20T10:00:00Z",
   "permissions": [
    {
     "type": "user",
     "role": "owner",
     "emailAddress": "ana@northwind.example.com"
    },
    {
     "type": "user",
     "role": "writer",
     "emailAddress": "priya@northwind.example.com"
    },
    {
     "type": "user",
     "role": "reader",
     "emailAddress": "tom@northwind.example.com"
    }
   ]
  },
  {
   "name": "Q4 media budget.xlsx",
   "path": "Campaigns/Q4",
   "owners": [
    {
     "emailAddress": "ana@northwind.example.com"
    }
   ],
   "modifiedTime": "2026-10-22T10:00:00Z",
   "permissions": [
    {
     "type": "user",
     "role": "owner",
     "emailAddress": "ana@northwind.example.com"
    },
    {
     "type": "user",
     "role": "writer",
     "emailAddress": "priya@northwind.example.com"
    }
   ]
  },
  {
   "name": "Q4 creative brief.docx",
   "path": "Campaigns/Q4",
   "owners": [
    {
     "emailAddress": "ana@northwind.example.com"
    }
   ],
   "modifiedTime": "2026-09-30T10:00:00Z",
   "permissions": [
    {
     "type": "user",
     "role": "owner",
     "emailAddress": "ana@northwind.example.com"
    },
    {
     "type": "user",
     "role": "writer",
     "emailAddress": "priya@northwind.example.com"
    }
   ]
  },
  {
   "name": "Q4 timeline.xlsx",
   "path": "Campaigns/Q4",
   "owners": [
    {
     "emailAddress": "ana@northwind.example.com"
    }
   ],
   "modifiedTime": "2026-10-25T10:00:00Z",
   "permissions": [
    {
     "type": "user",
     "role": "owner",
     "emailAddress": "ana@northwind.example.com"
    },
    {
     "type": "user",
     "role": "writer",
     "emailAddress": "tom@northwind.example.com"
    },
    {
     "type": "user",
     "role": "writer",
     "emailAddress": "priya@northwind.example.com"
    }
   ]
  },
  {
   "name": "Spring 2024 recap.pptx",
   "path": "Campaigns/Spring 2024",
   "owners": [
    {
     "emailAddress": "ana@northwind.example.com"
    }
   ],
   "modifiedTime": "2024-06-12T10:00:00Z",
   "permissions": [
    {
     "type": "user",
     "role": "owner",
     "emailAddress": "ana@northwind.example.com"
    },
    {
     "type": "user",
     "role": "reader",
     "emailAddress": "tom@northwind.example.com"
    }
   ]
  },
  {
   "name": "Spring 2024 assets list.xlsx",
   "path": "Campaigns/Spring 2024",
   "owners": [
    {
     "emailAddress": "ana@northwind.example.com"
    }
   ],
   "modifiedTime": "2024-05-03T10:00:00Z",
   "permissions": [
    {
     "type": "user",
     "role": "owner",
     "emailAddress": "ana@northwind.example.com"
    }
   ]
  },
  {
   "name": "Brand guidelines v3.pdf",
   "path": "Brand",
   "owners": [
    {
     "emailAddress": "ana@northwind.example.com"
    }
   ],
   "modifiedTime": "2026-08-14T10:00:00Z",
   "permissions": [
    {
     "type": "user",
     "role": "owner",
     "emailAddress": "ana@northwind.example.com"
    },
    {
     "type": "user",
     "role": "writer",
     "emailAddress": "studio@brightwave.example.com"
    },
    {
     "type": "user",
     "role": "reader",
     "emailAddress": "priya@northwind.example.com"
    }
   ]
  },
  {
   "name": "Logo source files.zip",
   "path": "Brand",
   "owners": [
    {
     "emailAddress": "ana@northwind.example.com"
    }
   ],
   "modifiedTime": "2026-02-02T10:00:00Z",
   "permissions": [
    {
     "type": "user",
     "role": "owner",
     "emailAddress": "ana@northwind.example.com"
    },
    {
     "type": "anyone",
     "role": "reader"
    }
   ]
  },
  {
   "name": "Monthly report Sept.xlsx",
   "path": "Reports",
   "owners": [
    {
     "emailAddress": "ana@northwind.example.com"
    }
   ],
   "modifiedTime": "2026-10-03T10:00:00Z",
   "permissions": [
    {
     "type": "user",
     "role": "owner",
     "emailAddress": "ana@northwind.example.com"
    },
    {
     "type": "user",
     "role": "writer",
     "emailAddress": "tom@northwind.example.com"
    },
    {
     "type": "user",
     "role": "reader",
     "emailAddress": "lee@northwind.example.com"
    }
   ]
  },
  {
   "name": "Monthly report Aug.xlsx",
   "path": "Reports",
   "owners": [
    {
     "emailAddress": "ana@northwind.example.com"
    }
   ],
   "modifiedTime": "2026-09-04T10:00:00Z",
   "permissions": [
    {
     "type": "user",
     "role": "owner",
     "emailAddress": "ana@northwind.example.com"
    },
    {
     "type": "user",
     "role": "writer",
     "emailAddress": "tom@northwind.example.com"
    }
   ]
  },
  {
   "name": "Monthly report July.xlsx",
   "path": "Reports",
   "owners": [
    {
     "emailAddress": "ana@northwind.example.com"
    }
   ],
   "modifiedTime": "2026-08-05T10:00:00Z",
   "permissions": [
    {
     "type": "user",
     "role": "owner",
     "emailAddress": "ana@northwind.example.com"
    },
    {
     "type": "user",
     "role": "writer",
     "emailAddress": "tom@northwind.example.com"
    },
    {
     "type": "user",
     "role": "reader",
     "emailAddress": "priya@northwind.example.com"
    }
   ]
  },
  {
   "name": "Team offsite agenda.docx",
   "path": "Reports",
   "owners": [
    {
     "emailAddress": "lee@northwind.example.com"
    }
   ],
   "modifiedTime": "2026-07-01T10:00:00Z",
   "permissions": [
    {
     "type": "user",
     "role": "owner",
     "emailAddress": "lee@northwind.example.com"
    }
   ]
  }
 ]
}

The page reads the paste (282 lines) as a Drive files list in JSON: 11 files owned by ana@northwind.example.com in 4 folders, with 1 file owned by someone else set aside. Counted as of 2026-11-07 (the leaving date): 2 files stale and 2 files shared outside.

The page warns: "1 file owned by someone else was set aside."

The folders (HandKit.form), biggest first, each listed one sent as one line (HandKit.folderLine):

IdFolderFilesStaleLast editedShared outsideInside collaborators
f1Campaigns/Q4402026-10-25nonepriya@northwind.example.com on 4, tom@northwind.example.com on 2
f2Reports302026-10-03nonetom@northwind.example.com on 3, lee@northwind.example.com on 1, priya@northwind.example.com on 1
f3Brand202026-08-142 (studio@brightwave.example.com, anyone with the link)priya@northwind.example.com on 1
f4Campaigns/Spring 20242all2024-06-12nonetom@northwind.example.com on 1

Input:

{
 "task": "handover",
 "goal": "Ana leaves the marketing team on Nov 7; Priya runs the Q4 launch from here",
 "people": "leaving: ana@northwind.example.com, files counted as of 2026-11-07; manager: lee@northwind.example.com; 11 files they own in 4 folders; stale means no edit in the 365 days before that date",
 "folders": "f1. Campaigns/Q4 | 4 files | 0 stale | last edited 2026-10-25 | nothing shared outside | inside collaborators: priya@northwind.example.com on 4, tom@northwind.example.com on 2\nf2. Reports | 3 files | 0 stale | last edited 2026-10-03 | nothing shared outside | inside collaborators: tom@northwind.example.com on 3, lee@northwind.example.com on 1, priya@northwind.example.com on 1\nf3. Brand | 2 files | 0 stale | last edited 2026-08-14 | 2 files shared outside (studio@brightwave.example.com, anyone with the link) | inside collaborators: priya@northwind.example.com on 1\nf4. Campaigns/Spring 2024 | 2 files | all stale | last edited 2024-06-12 | nothing shared outside | inside collaborators: tom@northwind.example.com on 1",
 "others": "none"
}

Reply:

{
 "task": "handover",
 "verdict": "blocked",
 "plan": [
  {
   "folder": "f1",
   "action": "transfer",
   "to": "priya@northwind.example.com",
   "why": "Priya is on 4 of its files, more than anyone else, so Campaigns/Q4 goes to them."
  },
  {
   "folder": "f2",
   "action": "transfer",
   "to": "tom@northwind.example.com",
   "why": "Tom is on 3 of its files, more than anyone else, so Reports goes to them."
  },
  {
   "folder": "f3",
   "action": "review",
   "to": "",
   "why": "Brand has 2 files shared outside, with studio@brightwave.example.com and anyone with the link able to open them."
  },
  {
   "folder": "f4",
   "action": "archive",
   "to": "lee@northwind.example.com",
   "why": "Campaigns/Spring 2024 is all stale, last edited 2024-06-12, so it goes to lee@northwind.example.com to keep."
  }
 ],
 "note": "The handover cannot start yet: check Brand's outside sharing first, then move Campaigns/Q4, with 4 files, to priya@northwind.example.com."
}

The plan the page trusts (ReplyKit.trusted), folder by folder, next to the action and receiver its own rule gives (HandKit.expectAction, HandKit.expectTo):

FolderActionToThe ruleWhy
Campaigns/Q4 (f1)transferpriya@northwind.example.comtransfer to priya@northwind.example.comPriya is on 4 of its files, more than anyone else, so Campaigns/Q4 goes to them.
Reports (f2)transfertom@northwind.example.comtransfer to tom@northwind.example.comTom is on 3 of its files, more than anyone else, so Reports goes to them.
Brand (f3)review (review sharing first)nobody yetreview (review sharing first), no receiver until the sharing is checkedBrand has 2 files shared outside, with studio@brightwave.example.com and anyone with the link able to open them.
Campaigns/Spring 2024 (f4)archive (archive with the manager)lee@northwind.example.comarchive (archive with the manager) to lee@northwind.example.comCampaigns/Spring 2024 is all stale, last edited 2024-06-12, so it goes to lee@northwind.example.com to keep.

The verdict: blocked - "Sharing needs a check first"; it agrees with the sharing.

The note: "The handover cannot start yet: check Brand's outside sharing first, then move Campaigns/Q4, with 4 files, to priya@northwind.example.com."

The page's check (ReplyKit.check): kept.

No notes.

An account executive (CSV)

The form: who is leaving: left blank; the manager: dana@harborlane.example.net; the leaving date: 11/14/2026; anything to know: Tom's last day is Nov 14.

The file list as pasted:

Name,Folder,Owner,Last modified,Shared with
Alderpine renewal.docx,Accounts/Alderpine,tom@harborlane.example.net,2026-10-28,raj@harborlane.example.net; kim@harborlane.example.net
Alderpine pricing.xlsx,Accounts/Alderpine,tom@harborlane.example.net,2026-10-30,raj@harborlane.example.net
Glimmerwick QBR.pptx,Accounts/Glimmerwick,tom@harborlane.example.net,2026-09-15,kim@harborlane.example.net
Glimmerwick contacts.xlsx,Accounts/Glimmerwick,tom@harborlane.example.net,2026-08-02,kim@harborlane.example.net; raj@harborlane.example.net
Proposal - Tallowbrook.docx,Proposals,tom@harborlane.example.net,2026-10-11,freelance.writer@example.org
Proposal template.docx,Proposals,tom@harborlane.example.net,2026-06-20,raj@harborlane.example.net
Pipeline 2023.xlsx,Old pipeline,tom@harborlane.example.net,2024-01-15,dana@harborlane.example.net
Lost deals 2023.xlsx,Old pipeline,tom@harborlane.example.net,2023-12-02,
Call notes.docx,Notes,tom@harborlane.example.net,2026-10-31,
Ideas.docx,Notes,tom@harborlane.example.net,2026-07-07,
Commission plan 2026.pdf,HR,dana@harborlane.example.net,2026-01-10,tom@harborlane.example.net

The page reads the paste (12 lines) as a comma-separated export with a header row: 10 files owned by tom@harborlane.example.net (the most common owner, as no address was given) in 5 folders, with 1 file owned by someone else set aside. Counted as of 2026-11-14 (the leaving date): 2 files stale and 1 file shared outside.

The page warns: "1 file owned by someone else was set aside. tom@harborlane.example.net (the most common owner) is taken as the person leaving."

The folders (HandKit.form), biggest first, each listed one sent as one line (HandKit.folderLine):

IdFolderFilesStaleLast editedShared outsideInside collaborators
f1Accounts/Alderpine202026-10-30noneraj@harborlane.example.net on 2, kim@harborlane.example.net on 1
f2Accounts/Glimmerwick202026-09-15nonekim@harborlane.example.net on 2, raj@harborlane.example.net on 1
f3Notes202026-10-31nonenone
f4Old pipeline2all2024-01-15nonedana@harborlane.example.net on 1
f5Proposals202026-10-111 (freelance.writer@example.org)raj@harborlane.example.net on 1

Input:

{
 "task": "handover",
 "goal": "Tom's last day is Nov 14",
 "people": "leaving: tom@harborlane.example.net, files counted as of 2026-11-14; manager: dana@harborlane.example.net; 10 files they own in 5 folders; stale means no edit in the 365 days before that date",
 "folders": "f1. Accounts/Alderpine | 2 files | 0 stale | last edited 2026-10-30 | nothing shared outside | inside collaborators: raj@harborlane.example.net on 2, kim@harborlane.example.net on 1\nf2. Accounts/Glimmerwick | 2 files | 0 stale | last edited 2026-09-15 | nothing shared outside | inside collaborators: kim@harborlane.example.net on 2, raj@harborlane.example.net on 1\nf3. Notes | 2 files | 0 stale | last edited 2026-10-31 | nothing shared outside | no inside collaborators\nf4. Old pipeline | 2 files | all stale | last edited 2024-01-15 | nothing shared outside | inside collaborators: dana@harborlane.example.net on 1\nf5. Proposals | 2 files | 0 stale | last edited 2026-10-11 | 1 file shared outside (freelance.writer@example.org) | inside collaborators: raj@harborlane.example.net on 1",
 "others": "none"
}

Reply:

{
 "task": "handover",
 "verdict": "blocked",
 "plan": [
  {
   "folder": "f1",
   "action": "transfer",
   "to": "raj@harborlane.example.net",
   "why": "Raj is on 2 of its files, more than anyone else, so Accounts/Alderpine goes to them."
  },
  {
   "folder": "f2",
   "action": "transfer",
   "to": "kim@harborlane.example.net",
   "why": "Kim is on 2 of its files, more than anyone else, so Accounts/Glimmerwick goes to them."
  },
  {
   "folder": "f3",
   "action": "manager",
   "to": "dana@harborlane.example.net",
   "why": "Notes has no inside collaborators, so it goes to dana@harborlane.example.net instead."
  },
  {
   "folder": "f4",
   "action": "archive",
   "to": "dana@harborlane.example.net",
   "why": "Old pipeline is all stale, last edited 2024-01-15, so it goes to dana@harborlane.example.net to keep."
  },
  {
   "folder": "f5",
   "action": "review",
   "to": "",
   "why": "Proposals has 1 file shared outside with freelance.writer@example.org, who can open it."
  }
 ],
 "note": "The handover cannot start yet: check Proposals' outside sharing first, then move Accounts/Alderpine, with 2 files, to raj@harborlane.example.net."
}

The plan the page trusts (ReplyKit.trusted), folder by folder, next to the action and receiver its own rule gives (HandKit.expectAction, HandKit.expectTo):

FolderActionToThe ruleWhy
Accounts/Alderpine (f1)transferraj@harborlane.example.nettransfer to raj@harborlane.example.netRaj is on 2 of its files, more than anyone else, so Accounts/Alderpine goes to them.
Accounts/Glimmerwick (f2)transferkim@harborlane.example.nettransfer to kim@harborlane.example.netKim is on 2 of its files, more than anyone else, so Accounts/Glimmerwick goes to them.
Notes (f3)manager (give to the manager)dana@harborlane.example.netmanager (give to the manager) to dana@harborlane.example.netNotes has no inside collaborators, so it goes to dana@harborlane.example.net instead.
Old pipeline (f4)archive (archive with the manager)dana@harborlane.example.netarchive (archive with the manager) to dana@harborlane.example.netOld pipeline is all stale, last edited 2024-01-15, so it goes to dana@harborlane.example.net to keep.
Proposals (f5)review (review sharing first)nobody yetreview (review sharing first), no receiver until the sharing is checkedProposals has 1 file shared outside with freelance.writer@example.org, who can open it.

The verdict: blocked - "Sharing needs a check first"; it agrees with the sharing.

The note: "The handover cannot start yet: check Proposals' outside sharing first, then move Accounts/Alderpine, with 2 files, to raj@harborlane.example.net."

The page's check (ReplyKit.check): kept.

No notes.

A postdoc (plain lines)

The form: who is leaving: mei@riverlab.example.org; the manager: ken@riverlab.example.org; the leaving date: 2026-12-18; anything to know: Mei finishes her postdoc in December.

The file list as pasted:

Lab protocols/PCR v4.docx | 2026-11-30 | sam@riverlab.example.org
Lab protocols/Imaging setup.docx | 2026-10-02 | sam@riverlab.example.org, ola@riverlab.example.org
Lab protocols/Freezer map.xlsx | 2026-12-01 | sam@riverlab.example.org
Grant 2026/Aims draft.docx | 2026-12-10 | ola@riverlab.example.org, sam@riverlab.example.org
Grant 2026/Budget.xlsx | 2026-12-11 | ola@riverlab.example.org, ken@riverlab.example.org
Grant 2026/Biosketch Mei.docx | 2026-11-20 | ola@riverlab.example.org
Data 2022/Run 1 raw.csv | 2022-09-14
Data 2022/Run 2 raw.csv | 2022-10-03 | sam@riverlab.example.org
Data 2022/Analysis notes.docx | 2023-01-20
Personal drafts/Paper outline.docx | 2026-12-15
Personal drafts/Talk slides.pptx | 2026-11-02

The page reads the paste (11 lines) as plain lines split by |: 11 files owned by mei@riverlab.example.org in 4 folders. Counted as of 2026-12-18 (the leaving date): 3 files stale and 0 files shared outside.

The folders (HandKit.form), biggest first, each listed one sent as one line (HandKit.folderLine):

IdFolderFilesStaleLast editedShared outsideInside collaborators
f1Data 20223all2023-01-20nonesam@riverlab.example.org on 1
f2Grant 2026302026-12-11noneola@riverlab.example.org on 3, ken@riverlab.example.org on 1, sam@riverlab.example.org on 1
f3Lab protocols302026-12-01nonesam@riverlab.example.org on 3, ola@riverlab.example.org on 1
f4Personal drafts202026-12-15nonenone

Input:

{
 "task": "handover",
 "goal": "Mei finishes her postdoc in December",
 "people": "leaving: mei@riverlab.example.org, files counted as of 2026-12-18; manager: ken@riverlab.example.org; 11 files they own in 4 folders; stale means no edit in the 365 days before that date",
 "folders": "f1. Data 2022 | 3 files | all stale | last edited 2023-01-20 | nothing shared outside | inside collaborators: sam@riverlab.example.org on 1\nf2. Grant 2026 | 3 files | 0 stale | last edited 2026-12-11 | nothing shared outside | inside collaborators: ola@riverlab.example.org on 3, ken@riverlab.example.org on 1, sam@riverlab.example.org on 1\nf3. Lab protocols | 3 files | 0 stale | last edited 2026-12-01 | nothing shared outside | inside collaborators: sam@riverlab.example.org on 3, ola@riverlab.example.org on 1\nf4. Personal drafts | 2 files | 0 stale | last edited 2026-12-15 | nothing shared outside | no inside collaborators",
 "others": "none"
}

Reply:

{
 "task": "handover",
 "verdict": "ready",
 "plan": [
  {
   "folder": "f1",
   "action": "archive",
   "to": "ken@riverlab.example.org",
   "why": "Data 2022 is all stale, last edited 2023-01-20, so it goes to ken@riverlab.example.org to keep."
  },
  {
   "folder": "f2",
   "action": "transfer",
   "to": "ola@riverlab.example.org",
   "why": "Ola is on 3 of its files, more than anyone else, so Grant 2026 goes to them."
  },
  {
   "folder": "f3",
   "action": "transfer",
   "to": "sam@riverlab.example.org",
   "why": "Sam is on 3 of its files, more than anyone else, so Lab protocols goes to them."
  },
  {
   "folder": "f4",
   "action": "manager",
   "to": "ken@riverlab.example.org",
   "why": "Personal drafts has no inside collaborators, so it goes to ken@riverlab.example.org instead."
  }
 ],
 "note": "The handover can start: move Grant 2026, with 3 files, to ola@riverlab.example.org first."
}

The plan the page trusts (ReplyKit.trusted), folder by folder, next to the action and receiver its own rule gives (HandKit.expectAction, HandKit.expectTo):

FolderActionToThe ruleWhy
Data 2022 (f1)archive (archive with the manager)ken@riverlab.example.orgarchive (archive with the manager) to ken@riverlab.example.orgData 2022 is all stale, last edited 2023-01-20, so it goes to ken@riverlab.example.org to keep.
Grant 2026 (f2)transferola@riverlab.example.orgtransfer to ola@riverlab.example.orgOla is on 3 of its files, more than anyone else, so Grant 2026 goes to them.
Lab protocols (f3)transfersam@riverlab.example.orgtransfer to sam@riverlab.example.orgSam is on 3 of its files, more than anyone else, so Lab protocols goes to them.
Personal drafts (f4)manager (give to the manager)ken@riverlab.example.orgmanager (give to the manager) to ken@riverlab.example.orgPersonal drafts has no inside collaborators, so it goes to ken@riverlab.example.org instead.

The verdict: ready - "The handover can start"; it agrees with the sharing.

The note: "The handover can start: move Grant 2026, with 3 files, to ola@riverlab.example.org first."

The page's check (ReplyKit.check): kept.

No notes.

The CSV for the first sample

What HandKit.toCSV(form, t.plan) writes for "A marketing manager (Drive JSON)" above: one row per file the person leaving owns, biggest folder first, with the trusted plan and who receives it (lines end in CRLF). A file in a folder past the list has no id and no plan.

folder_id,folder,file,last_edited,stale,inside_collaborators,shared_outside,plan,to
f1,Campaigns/Q4,Q4 launch plan.docx,2026-10-20,,priya@northwind.example.com; tom@northwind.example.com,,transfer,priya@northwind.example.com
f1,Campaigns/Q4,Q4 media budget.xlsx,2026-10-22,,priya@northwind.example.com,,transfer,priya@northwind.example.com
f1,Campaigns/Q4,Q4 creative brief.docx,2026-09-30,,priya@northwind.example.com,,transfer,priya@northwind.example.com
f1,Campaigns/Q4,Q4 timeline.xlsx,2026-10-25,,tom@northwind.example.com; priya@northwind.example.com,,transfer,priya@northwind.example.com
f2,Reports,Monthly report Sept.xlsx,2026-10-03,,tom@northwind.example.com; lee@northwind.example.com,,transfer,tom@northwind.example.com
f2,Reports,Monthly report Aug.xlsx,2026-09-04,,tom@northwind.example.com,,transfer,tom@northwind.example.com
f2,Reports,Monthly report July.xlsx,2026-08-05,,tom@northwind.example.com; priya@northwind.example.com,,transfer,tom@northwind.example.com
f3,Brand,Brand guidelines v3.pdf,2026-08-14,,priya@northwind.example.com,studio@brightwave.example.com,review,
f3,Brand,Logo source files.zip,2026-02-02,,,anyone with the link,review,
f4,Campaigns/Spring 2024,Spring 2024 recap.pptx,2024-06-12,yes,tom@northwind.example.com,,archive,lee@northwind.example.com
f4,Campaigns/Spring 2024,Spring 2024 assets list.xlsx,2024-05-03,yes,,,archive,lee@northwind.example.com

Build the input yourself

Load the engine in node (global.window = global; require("./handkit.js"); require("./replykit.js"); require("./waffle.js")), then:

const K = HandKit, R = ReplyKit;
const form = K.form({ leaver: "jo@maplecourt.org", manager: "max@maplecourt.org", when: "2026-11-30",
  goal: "Jo leaves at the end of November",
  report: "Clients/Plan.docx | 2026-10-20 | sam@maplecourt.org\nClients/Notes.docx | 2026-09-02 | sam@maplecourt.org, pat@gmail.com\nArchive/Budget 2023.xlsx | 2024-03-01" });
// form.files: every file the person leaving owns [{name, path, folder, modified, inside, outside, personal, link, out, stale}]
// form.folders: biggest first [{id, name, count, staleCount, allStale, outCount, outWho, people, receiver, last}]
// form.items: the first 20 (f1 ...); form.rest: the ones past the list
K.expectVerdict(form); K.expectAction(form.items[0]); K.expectTo(form.items[0], form)   // the page's own verdict and rule, no run needed
const built = K.buildInput(form);     // built.ok is false, with built.note, until there is a list, a leaver and a manager
// built.items: the listed folders; built.warn: what the page warns about
const wire = K.toWire(built.input);   // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output));
// const t = R.trusted(reply, built)       // {acts: {f1: action}, plan: {f1: {action, to}}, why, dropped, missing}
// R.check(reply, built)                    // {status: kept or flagged, notes}
// K.toCSV(form, t.plan)                    // every file with its folder, date, collaborators and plan
// HandWaffle.draw(form, t.plan, {dark: false})   // the waffle as an SVG string

Form keys: report (the pasted file list, any shape above), leaver (the address of the person leaving, or empty for the most common owner), manager (the manager's address), when (the leaving date, or empty) and goal (what you already know, or empty). Nothing is moved in Drive by the engine: it only reads the text you give it.

Step 1 — a tiny client helper

One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://hand-off-files.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"hand-off-files"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "handover", "goal": "Jo leaves at the end of November", "people": "leaving: jo@maplecourt.org, files counted as of 2026-11-30; manager: max@maplecourt.org; 3 files they own in 2 folders; stale means no edit in the 365 days before that date", "folders": "f1. Clients | 2 files | 0 stale | last edited 2026-10-20 | 1 file shared outside (pat@gmail.com) | inside collaborators: sam@maplecourt.org on 2\nf2. Archive | 1 file | all stale | last edited 2024-03-01 | nothing shared outside | no inside collaborators", "others": "none"}
# - see the field table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses hand-off-files:<task>:<inputHash>:a<attempt> (the one task is handover), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="hand-off-files:handover:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r '.verdict, .note, (.plan[] | "\(.folder) \(.action) \(.to)")' reply.json

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: hand-off-files:handover:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json