---
title: Join a Desk
description: Open a Desk so teammates can serve a lane from their own accounts, join one as a worker, leave, and change the join settings. Since 0.8.0. Every command output shown is what the CLI printed.
sidebar:
  order: 5
---

> Since 0.8.0.

This guide covers both sides. The owner opens a Desk. A joiner serves one lane of it from their own accounts and connectors, so the work runs as them and their approvals are theirs.

## Before you start

- The owner has a Desk with a `definition.json` (see [Add a Desk to a routine](/guides/add-a-desk)).
- The owner has shared the Desk artifact with the joiner as a Contributor. Without that the CLI says "Ask the owner to share this Desk with you as a Contributor."
- Both have the `routineos.py` CLI. `$RO` below stands for `python3 skills/routineos/scripts/routineos.py`.

## Owner: open the Desk

Add one `join` block to the Desk-level `definition.json`. A Desk has one `join` block; a second fails `compile`.

```json
"join": {
  "version": 1,
  "open": true,
  "summary": "Draft replies to inbound sales mail from your own mailbox.",
  "lanes": [{
    "id": "sender",
    "label": "Sender",
    "kind": "mail-draft",
    "match": {"type": "role", "field": "owner", "equals": "{mailbox}"},
    "params": {"mailbox": "email"},
    "connectors": ["gmail"],
    "schedule": {"default": "0 22 * * 1,4", "min_interval_hours": 24},
    "gate": "none"
  }],
  "cap": {"scope": "account", "maxRunsPerDay": 1}
}
```

Every key is described in the [definition schema](/reference/definition-schema#join). Then seed as usual. Pass `--url` with the Desk's address so joiners get a link; the seed stores it in `meta/join`.

```
$RO seed <dir> --out seed --url <desk_url>
```

To open or close an existing Desk later, the owner runs:

```
$RO join-settings <dir> --open --out out --docs <snapshot.json> --by <ownerId>
$RO join-settings <dir> --closed --out out --docs <snapshot.json> --by <ownerId>
```

It rewrites `join.open` in `definition.json` and plans the `meta/join` write. Only the owner can run it, and `--by` must equal `meta/owner.by`. A Desk with no `join` block refuses: "this Desk has no join block; answer the open-to-join question in build first". Applying the batch is the owner's decision.

## Joiner: see the lanes

```
$RO join <desk_url> --docs <snapshot.json>
```

With no `--lane`, `join` lists the lanes and which are free. A closed Desk refuses with "This Desk is not open to join. Ask its owner to open it." A lane served by someone else refuses with "This lane is served by someone else since <ts>. Ask the Desk owner to release it."

## Joiner: take a lane

```
$RO join <desk_url> --docs <snapshot.json> --lane sender \
  --param mailbox=you@example.com --connected gmail \
  --schedule "0 22 * * 1,4" --tz Australia/Sydney \
  --gate-email you@example.com --by <yourDeskId> --name "Your Name" --out join-out
```

`join` checks that the lane is free, that the connectors the lane needs are present, that the schedule keeps the lane's minimum spacing, the timezone (default UTC), the gate email, `--by` and `--out`. It then prints the permission screen and writes `worker-routine.json`. Your Desk id is on the [Workers](/desk/workers) page with a Copy control.

Create the scheduled routine from `worker-routine.json`, then run `join` again with `--routine-id trig_...` to also plan the `workers/<laneKey>` batch that records you as the holder. Apply that batch yourself.

The first run is report-only: it plans work but acts on nothing until a `people/<deskId>/decisions/verified--<laneKey>` document exists, written on your word after you read a run.

## What a worker run does

A run reads its items with `worker-items` and writes only its own lane:

```
$RO worker-items --docs <snapshot.json> --worker <laneKey> --by <id> --out items.json
$RO budget-inputs --docs <snapshot.json> --worker <laneKey> --out budget
$RO write --docs <snapshot.json> --run run.json --out out --worker <laneKey> --artifactdata
```

- `items.json` has `stopped`, `verified`, `todo`, `approved` and `waiting`.
- `write --worker` may touch only the lane's own `workers/`, `outcomes/` and `pending/` paths. When the lane is stopped it prints "stopped: <reason>; nothing written" and exits 0. Reasons are "no worker document", "left", "served by someone else" and "released".
- The run input needs `laneKey`, `by` and `runId`, and optionally `outcomes`, `gates`, `budget` and `health`.
- A `mail-draft` lane drafts and you send. A `mail-send` lane has a `send` gate; you approve on the Desk, and only you can.
- Outcomes are `drafted`, `sent`, `skipped` or `failed`.

The daily cap applies per `cap.scope`. A run counts itself before it does work.

## Joiner: leave

```
$RO leave <desk_url> --docs <snapshot.json> --by <yourDeskId> --out leave-out
```

It plans `status: left` and `leftAt` for your lanes and prints the recorded routine ids. Disable each scheduled routine after you confirm (RemoteTrigger with `enabled: false`); `leave` does not do it for you. If you serve no lane it says "You do not serve any lane on this Desk."

## Owner: release a lane

On the Workers page, Release (with a confirm) writes `people/<ownerId>/decisions/release--<laneKey>` naming the holder. The release is final for that holder. To let them back, remove that document.

## Check it worked

- The Workers page shows your name on the lane after the first run.
- `worker-items` shows `stopped` empty and `verified` set once you have accepted a run.
- Re-run `join` without `--lane`: your lane no longer shows as free.

## Common pitfalls

- **Forgetting to share the Desk.** The joiner must be a Contributor first.
- **Applying the owner's batches from a joiner's session.** `meta/` is owner-written and `people/` is self-written.
- **Treating the approval email as the approval.** It is a notice only. The click on the Desk is the approval, and editing the gate after the click voids it.
- **Expecting a released joiner's counters to count.** The `desk` cap counts only workers still serving.

## Related

- [Workers page](/desk/workers)
- [Ownership](/concepts/ownership)
- [Learning](/concepts/learning)
- [CLI reference](/reference/cli)
