Inspiration

Every team has that moment: someone goes OOO or just gets slammed, and their open tasks either sit untouched or get dumped on whoever happens to be free. We wanted Slack to handle that handoff itself, but not as one bot bossily reassigning work top-down. Instead, we built it as two agents negotiating on behalf of two people. The person going OOO has an agent proposing their tasks. The candidate teammate has an agent deciding whether to accept, based on their own actual workload. A human still has the final say, ✅ or ❌, before anything actually moves. When the OOO person's back, the bot flags whether anything's still pending or was left unassigned, and they can mention the bot directly to search the workspace live for anything they need to catch up on.

What it does

The OOO Negotiation Agent gives every person their own agent inside Slack. When someone goes OOO or signals they're overloaded (a slash command, or just a message like "I'm swamped this week"), their agent:

  1. Extracts their open commitments from recent messages using an LLM
  2. Selects candidates to take over each task, scored by current workload and related past work
  3. Negotiates with each candidate's agent using a deterministic rules engine — not an LLM — so every outcome is explainable: workload threshold, priority bump, capped rounds, then escalate to a human if nobody accepts
  4. Posts the negotiation visibly in Slack as a threaded sequence of propose/counter/accept messages, so the team watches it happen instead of getting a single opaque summary
  5. Waits for real human confirmation — an agent accepting on someone's behalf is only a proposal. Nothing is reassigned until the actual person reacts with ✅. If they don't respond within 45 seconds, it escalates to a human instead of leaving the task in limbo

On top of the negotiation flow, mentioning the bot directly (@ooo-negotiator onboarding document) triggers a live, permission-aware search across the workspace using Slack's Real-Time Search API — so when a task escalates and a human has to manually decide who takes it, they can see real recent messages, authors, and channels instead of guessing.

Additional commands (/escalated, /resolve-escalation, /back-from-ooo) round out the loop so nothing silently falls through the cracks.

How we built it

  • Stack: Node.js, @slack/bolt (Socket Mode), Groq API (llama-3.3-70b-versatile) for extraction, Slack's Real-Time Search API for live workspace lookups, JSON-file-backed state storage
  • Architecture: seven core modules with frozen JSON contracts defined on day one — PersonState, ReassignmentCandidate, NegotiationEvent, NegotiationTrace — so extraction, negotiation, state, and rendering could be built and tested independently against mock data before wiring together
  • Negotiation logic is deliberately deterministic — a two-variable rule (workload threshold + priority bump, capped rounds) rather than LLM-to-LLM negotiation. A decision that reassigns someone's real work can't afford to be unpredictable
  • Confirm-gating is a hard requirement, not a nice-to-have — every reassignment sits in a "pending" state until a real ✅ reaction (or a manual /confirm-reassign command as a backup path) is received. Nothing moves on an agent's decision alone
  • Real-Time Search integration — calls Slack's assistant.search.context method via the SDK's generic apiCall() escape hatch (the installed SDK doesn't have this brand-new endpoint wrapped as a named method yet), authenticated with the short-lived action_token Slack attaches directly to app_mention events

Challenges we ran into

  • Catching our own bug before it shipped: in an early version, the bot reassigned tasks the instant negotiation resolved — before the actual human had confirmed anything. That defeated the entire "agents propose, humans decide" premise. We caught it during review, rebuilt reassignment to sit in a pending state, and wrote a dedicated test proving the fix: the task stays with the original owner until an explicit confirm, verified before and after.
  • Getting Real-Time Search actually working: the documentation on where Slack attaches the action_token for assistant.search.context was ambiguous — some pages implied it only appears on a nested assistant_thread object, others implied it's top-level. We resolved this empirically by logging a real live event payload rather than trusting docs alone, and found it's a top-level field on the event. We also discovered the installed Slack SDK doesn't have this new method wrapped at all (checked every published version, including release candidates), so we called it through the SDK's generic method-call escape hatch instead.
  • Escalated tasks disappearing: initially, a task that escalated (no candidate accepted) got announced once and then had no way to be found again. We added persistent tracking and /escalated / /resolve-escalation commands so nothing gets lost.

Accomplishments we're proud of

  • A negotiation engine and full pipeline backed by six automated test suites — negotiation logic, extraction accuracy, state + candidate integration, the confirm-gate fix, escalation handling, and the Slack-facing modules (tested against a mock Slack client) — all passing with zero regressions.
  • Every reassignment in the demo requires a real, verifiable human confirmation — not a claim, an actual gated code path with a test proving it.
  • Getting Real-Time Search genuinely working end-to-end, including resolving an SDK compatibility gap and an undocumented payload detail through direct empirical testing rather than giving up when the docs didn't line up.

What we learned

  • Deterministic logic is worth defending even when an LLM would be "easier" to wire up — explainability matters more than flexibility for a decision that affects someone's actual workload.
  • A caught bug is only worth catching if you also write the test that proves it stays fixed.
  • Visible process is a feature, not a nice-to-have — the negotiation trace is what makes this trustworthy, not just functional.
  • When platform documentation and reality disagree, trust a real logged payload over the docs.

What's next

  • A real skill-matching signal for candidate selection, beyond workload + keyword overlap
  • Persisting state to a proper database instead of local JSON for multi-workspace deployment
  • Submitting to the Slack Marketplace as a lightweight, install-anywhere app

Built With

Share this project:

Updates