> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getthread.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Assign and schedule

> The dispatcher's assignment toolkit: Available-for-dispatch, Next thread vs Auto-assign profiles, Planner's Triage and member swimlanes, and XLA timers.

export const MarkComplete = ({id, signalDone = false}) => {
  const KEY = "thread-onboarding-completed";
  const read = () => {
    try {
      return JSON.parse(localStorage.getItem(KEY) || "[]");
    } catch (e) {
      return [];
    }
  };
  const [localDone, setLocalDone] = useState(false);
  const done = signalDone || localDone;
  useEffect(() => {
    const sync = () => setLocalDone(read().includes(id));
    sync();
    window.addEventListener("thread-onboarding-updated", sync);
    return () => window.removeEventListener("thread-onboarding-updated", sync);
  }, [id]);
  const toggle = () => {
    if (signalDone) return;
    const list = read();
    const next = list.includes(id) ? list.filter(x => x !== id) : [...list, id];
    try {
      localStorage.setItem(KEY, JSON.stringify(next));
    } catch (e) {}
    setLocalDone(next.includes(id));
    window.dispatchEvent(new Event("thread-onboarding-updated"));
  };
  return <button onClick={toggle} style={{
    width: "100%",
    padding: "0.7rem 1rem",
    marginTop: "1.5rem",
    borderRadius: "8px",
    border: "1px solid " + (done ? "#00B398" : "rgba(128,128,128,0.35)"),
    background: done ? "rgba(0,179,152,0.12)" : "transparent",
    color: done ? "#00B398" : "inherit",
    fontWeight: 600,
    fontSize: "0.95rem",
    cursor: "pointer",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    gap: "0.5rem"
  }}>
      <span aria-hidden="true">{done ? "✓" : "○"}</span>
      {done ? "Completed" : "Mark as complete"}
    </button>;
};

export const ByRoleProgress = () => {
  useEffect(() => {
    const KEY = "thread-onboarding-completed";
    const TEAL = "#00B398";
    const BASE = "/start-here/roles/";
    const DONE_ICON = '<svg class="thread-rc-doneicon size-4" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#00B398" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' + '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>' + '<polyline points="22 4 12 14.01 9 11.01"/></svg>';
    const COURSES = [{
      slug: "technician",
      steps: ["why-thread", "get-around-inbox", "day-one-setup", "daily-workflows", "ai-toolkit", "notifications-and-customers"]
    }, {
      slug: "dispatcher",
      steps: ["why-dispatch-in-thread", "read-the-board", "assign-and-schedule", "ai-for-dispatch"]
    }, {
      slug: "service-ops-manager",
      steps: ["set-up-the-desk", "sla-and-dispatch", "analytics-and-qa", "roll-out-and-adopt"]
    }, {
      slug: "csm-account-manager",
      steps: ["client-intelligence-and-health", "qbrs-and-reporting", "retention-and-expansion"]
    }, {
      slug: "security-compliance-owner",
      steps: ["security-work-in-thread", "data-and-compliance", "security-runbooks"]
    }, {
      slug: "sales-business-development",
      steps: ["the-ai-service-pitch", "quote-and-handoff"]
    }, {
      slug: "msp-owner-leadership",
      steps: ["the-rollout", "outcomes-and-analytics", "scale-ai-service"]
    }, {
      slug: "automation-engineer",
      steps: ["flows-and-intents", "thread-mcp-and-api", "build-with-super-magic"]
    }];
    const COURSE_SLUGS = new Set(COURSES.map(c => c.slug));
    const readLocal = () => {
      try {
        return JSON.parse(localStorage.getItem(KEY) || "[]");
      } catch (e) {
        return [];
      }
    };
    const isDone = (done, id) => done.includes(id);
    const decorate = () => {
      const done = readLocal();
      const scope = document.getElementById("sidebar-content") || document;
      const links = [...scope.querySelectorAll('a[href*="' + BASE + '"]')].filter(a => !a.closest("main"));
      links.forEach(a => {
        const m = (a.getAttribute("href") || "").match(/\/start-here\/roles\/([^/?#]+)\/([^/?#]+)/);
        if (!m) return;
        if (!COURSE_SLUGS.has(m[1])) return;
        const id = m[1] + "/" + m[2];
        const d = isDone(done, id);
        const has = a.querySelector(".thread-rc-check");
        if (d && !has) {
          const s = document.createElement("span");
          s.className = "thread-rc-check";
          s.textContent = "✓ ";
          s.style.color = TEAL;
          s.style.fontWeight = "700";
          a.insertBefore(s, a.firstChild);
        } else if (!d && has) {
          has.remove();
        }
      });
      COURSES.forEach(course => {
        const li = scope.querySelector('li[id="' + BASE + course.slug + '"]');
        if (!li) return;
        const row = li.querySelector(":scope > button") || li.querySelector(":scope > a") || li.firstElementChild;
        if (!row) return;
        const nameSpan = [...row.querySelectorAll("span")].find(s => s.textContent.trim() && !s.classList.contains("thread-rc-coursecheck"));
        if (!nameSpan) return;
        const allDone = course.steps.length > 0 && course.steps.every(s => isDone(done, course.slug + "/" + s));
        const has = nameSpan.querySelector(".thread-rc-coursecheck");
        if (allDone && !has) {
          const s = document.createElement("span");
          s.className = "thread-rc-coursecheck";
          s.setAttribute("aria-hidden", "true");
          s.style.display = "inline-flex";
          s.style.alignItems = "center";
          s.style.verticalAlign = "-0.2em";
          s.style.marginRight = "0.35em";
          s.innerHTML = DONE_ICON;
          nameSpan.insertBefore(s, nameSpan.firstChild);
        } else if (!allDone && has) {
          has.remove();
        }
      });
    };
    decorate();
    const iv = setInterval(decorate, 600);
    window.addEventListener("thread-onboarding-updated", decorate);
    window.addEventListener("storage", decorate);
    return () => {
      clearInterval(iv);
      window.removeEventListener("thread-onboarding-updated", decorate);
      window.removeEventListener("storage", decorate);
    };
  }, []);
  return null;
};

<ByRoleProgress />

Reading the board tells you *what* needs to happen. This lesson is about making it happen: getting the right thread onto the right tech, scheduled for the right time, without letting an XLA slip. Thread gives you three levers — availability, auto-dispatch, and Planner — plus a live XLA stepper on every ticket so you always know what's tightening.

## Availability — who can take work right now

Before Thread pushes any work automatically, it needs to know who's actually at their desk. Each member sets their own status from the account menu in Inbox:

* **Available** — a **green dot** on their avatar; auto-dispatch can push new threads to them.
* **Offline** — a **grey dot**; auto-dispatch skips them.

As a dispatcher, treat that green dot as your capacity signal — it's the roster you're balancing against. Availability persists across sessions and devices, and it only affects **push** auto-dispatch; techs can still pull the next thread manually regardless. See [Set your availability for auto-dispatch](/inbox/available-for-dispatch).

<Tip>
  You don't have to chase people down in DMs to flip their switch — open a team's [Team Home](/inbox/team-home) page and toggle availability directly from the Members table. It's the same server-side state as each tech's account-menu switch, so both surfaces stay in sync. Team Home also shows each tech's live active-thread count against the profile's Max threads limit, so you can see who Auto-assign will actually pick next.
</Tip>

<Note>
  The **Available for dispatch** toggle only appears when your workspace has **push auto-dispatch** enabled. On pull-only workspaces, techs use a "Get next thread" button instead, and there's no availability switch to watch.
</Note>

## Auto-dispatch profiles — let Thread do the routine routing

[Auto-dispatch profiles](/inbox/auto-dispatch-profiles) decide how eligible tickets reach techs. Each profile targets one or more boards and runs in one of two modes:

| Mode            | How it assigns                                                                                                       | Best for                                                                 |
| --------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| **Next thread** | Threads wait in your dispatch statuses; a tech requests the next one, and **Thread scoring** decides which comes up. | Desks that already run a "grab the next ticket" workflow.                |
| **Auto-assign** | Thread pushes eligible threads to available techs automatically, based on their active workload.                     | Desks that want tickets on a tech's plate the moment they have capacity. |

**Next thread** ranks the queue with eight **Thread scoring** factors you weight (Not used → Highest): priority, ticket age, client-replied, sentiment, company type, contact type, agreement type, and source. **Auto-assign** instead caps each tech at a **max active threads** count (up to 80), measures load against the **active statuses** you pick per board, and — when no tech is eligible — either **retries every minute** or **applies a fallback status change**, whichever you choose. Both modes have a **Test run** panel so you can preview the order before you save.

<Info>
  **Availability of Auto-assign:** **Next thread** (pull) is available to everyone; **Auto-assign** (push) is rolling out — it shows a "Coming soon" tag until it's enabled for your workspace.
</Info>

<Warning>
  Auto-assign can **override any Flow that assigns tickets to specific members** — Thread shows a banner when you enter Auto-assign mode. Review and scope those member-assigning Flows before you turn Auto-assign on in production, or two systems will fight over the same tickets.
</Warning>

Configuring profiles is an admin job, but you're the one who lives with the result — so feed back what you see. If the scoring surfaces the wrong ticket next, or a max-threads cap is starving a fast tech, that's tuning you should flag.

## Planner — schedule the day by dragging

For work that needs to live on a calendar, [Planner](/inbox/using-planner) is where you dispatch by hand. It lays the board out as **swimlanes**:

* **Triage swimlane** — every **unassigned** active thread. This is your dispatch pile. Work it down by finding each thread an owner.
* **Member swimlanes** — one per tech, showing everything **assigned to them**, further split into spaces: **Overdue** (plans earlier than today), **Assigned** (owned but unscheduled), and a space per **Day**.

Dispatching is drag-and-drop:

<Steps>
  <Step title="Assign by dragging out of Triage">
    Drag a thread from the **Triage swimlane** into a member's lane. That assigns them as owner.
  </Step>

  <Step title="Schedule by dropping on a day or a time">
    Drop the thread into a **day space** to plan it for that day (this also assigns it if it was unassigned). Need a precise slot? Drop it onto a **time slot**, or click the card's calendar icon — times are in **15-minute** increments and default to a 30-minute block.
  </Step>

  <Step title="Reschedule, reassign, or unplan by dragging again">
    Drag between day spaces to change the day; drag into another member's lane to reassign. Dragging a planned card back to **Triage** or **Assigned** **removes its plan** (and the matching PSA entry) — Planner asks you to confirm, and every change shows an **undo** toast.
  </Step>
</Steps>

Planner syncs to your PSA automatically, so a scheduled thread shows up on the tech's PSA calendar too. Your changes reach the PSA in real time; inbound, **ConnectWise is real-time**, while **Autotask and HaloPSA refresh on a \~5-minute poll** (the **Refresh** button forces an immediate pull). Conflicts differ by PSA: **ConnectWise lets you double-book** (overlaps show as conflicts on the board); **Autotask and HaloPSA reject overlaps**. If you use TimeZest, you can book a customer-facing appointment straight from the composer with `/timezest`.

## XLA timers — never get surprised by a breach

Every ticket with an XLA carries a live [stepper](/inbox/sla-timers-and-response-settings) in Inbox, and reading it is core dispatcher hygiene. Each ticket can run two timers:

* **Response timer** — counts down to the first-reply deadline (and stops the moment a member sends that first reply).
* **Resolution timer** — counts down to the resolution deadline, scoped per board.

The stepper shows **real remaining time**, advances as the ticket moves through its lifecycle, and marks a ticket **breached** if it resolves after its deadline had already passed. Hover it to see **which XLA profile** is driving the deadline.

<Tip>
  Timers **pause automatically while the Triage Agent is engaged**, and due dates respect your [holidays and custom closures](/inbox/holidays-and-custom-closures) — so a thread in an AI-handled conversation, or one sitting over a closure, won't silently breach. When a paused timer resumes, it re-anchors to a correct deadline rather than inheriting a stale one. Sort your triage View so the tightest steppers float to the top and dispatch those first.
</Tip>

<MarkComplete id="dispatcher/assign-and-schedule" />

## Next

You can route, schedule, and protect XLAs by hand. Now let AI carry the routine so you focus on the judgment calls.

<Card title="AI for dispatch" icon="robot" href="/start-here/roles/dispatcher/ai-for-dispatch">
  Tune the Triage Agent, drive triage with Super Magic, and report on the board.
</Card>


## Related topics

- [Smart Dispatch](/skill-library/scheduling-and-dispatch/smart-dispatch.md)
- [Dispatcher](/start-here/roles/dispatcher.md)
- [Read the board](/start-here/roles/dispatcher/read-the-board.md)
