›
›
›
  1. docs
  2. ›
  3. byrcsc/laravel-assignment
1.x
Browse documentationOpenClose

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Assignments and slots
  • Assigning and offering
  • Selection policies
  • Scopes and rotation
  • Profiles
  • The queue
  • Offer cascades

Operations

  • Expiry and scheduling
  • Reading assignments
  • Notifications
  • Events and listeners
  • Concurrency guarantees

Reference

  • Configuration
  • API reference
  • Console commands
  • Exceptions
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Assignments and slots
  • Assigning and offering
  • Selection policies
  • Scopes and rotation
  • Profiles
  • The queue
  • Offer cascades

Operations

  • Expiry and scheduling
  • Reading assignments
  • Notifications
  • Events and listeners
  • Concurrency guarantees

Reference

  • Configuration
  • API reference
  • Console commands
  • Exceptions
  • Testing
  • Troubleshooting

byrcsc/laravel-assignment · 1.x

Offer cascades.

A declined or expired offer moves to the next candidate, skipping everyone who already turned it down.

An offer nobody answers is work that has stopped moving. A cascade keeps it moving: when an offer is declined or expires, the engine re-runs the selection against the remaining candidates and offers it to the next one.

offer to Eli   -> declined
offer to Fay   -> expired
offer to Ivo   -> accepted

Each step is a new row, so the assignable's history shows every crew who was asked and what they did.

Only profile offers cascade

A cascade needs to resolve candidates again later, which is exactly what a profile provides. So:

  • An offer created by autoAssign() on a model in offer mode gets a cascade_id and cascades.
  • An offer created by the fluent builder or by $model->offer($assignee) has no cascade_id. It ends when it is declined or expires, and nothing follows.

That is deliberate. An ad-hoc offer has no candidate list to fall back on, and guessing one would be worse than stopping.

Who gets skipped

Re-offers exclude every assignee who declined or let an offer expire in the same cascade. The exclusion is tracked by the cascade_id on the rows, so it survives the candidate list changing between steps.

The exclusion does not persist beyond the cascade. Once the cascade ends, a later attempt starts fresh with everyone eligible again, including the people who said no last time.

When the candidates run out

The cascade stops. Two things then happen:

  1. The assignable is queued, when the profile's queueWhenEmpty() returns true.
  2. OffersExhausted fires, carrying the last offer.
public function handle(OffersExhausted $event): void
{
    $event->lastOffer->assignable;   // what nobody would take
    $event->lastOffer->role;
}

Exhaustion is the signal that your candidate pool is too small, that everybody is busy, or that the work is unattractive. A listener is where an escalation belongs.

A later flush starts a new cascade with a new id, so the people who declined the first round are asked again.

A tick can exhaust and re-offer in one run. assignment:tick expires overdue offers first and flushes the queue second. An offer that exhausts its cascade during the expiry step is queued, and the flush in the same command can start a fresh cascade immediately, which may re-offer to somebody who declined moments earlier. Split the two steps if that is not what you want.

Declines cascade in the same request

A decline advances the cascade synchronously, inside the call:

$crew->decline($offer);

$callout->openAssignment('crew');   // already the next crew's offer

The assignee who declined gets no "assignment removed" notification, because they chose not to take it. The next candidate gets an "offer received" notification as usual.

Expiries cascade when the clock is advanced

An expired offer stops holding its slot the moment expires_at passes. The row still reads offered until something ends it, and the cascade has not moved on yet.

Two things advance it:

// routes/console.php
Schedule::command('assignment:tick')->everyMinute();
// config/assignment.php
'dispatch_expiry_jobs' => true,

The tick sweeps every overdue offer. The job option additionally dispatches one delayed job per offer at the moment it expires, which gives second-level precision instead of cron granularity. The tick stays as the safety net for jobs that were lost.

Delayed jobs are dispatched only for offers that have both a cascade_id and an expires_at, so an ad-hoc offer never queues one. A job whose offer was answered before it ran does nothing.

See expiry and scheduling.

What stops a cascade

  • The slot being filled. If something else took the slot between steps, the cascade stops rather than competing for it.
  • Reassignment or unassignment. Both write to the slot, which ends the cascade's claim on it.
  • Acceptance. The work found a holder.
  • Exhaustion. Nobody is left.

What it does not do

  • Limit the number of steps. A cascade runs until somebody accepts or the candidates are exhausted. Cap it by shortening the candidate list.
  • Back off between steps. The next offer is made immediately.
  • Remember refusals across cascades. That is what makes a later flush useful, and it means a crew who always declines is always asked again.
  • Cascade an ad-hoc offer. See above.

What to read next

  • Expiry and scheduling for the tick and the delayed jobs.
  • The queue for where an exhausted assignable waits.
  • Events and listeners for OffersExhausted and the rest.
PreviousThe queueNextExpiry and scheduling
View source

On this page

  1. Only profile offers cascade
  2. Who gets skipped
  3. When the candidates run out
  4. Declines cascade in the same request
  5. Expiries cascade when the clock is advanced
  6. What stops a cascade
  7. What it does not do
  8. What to read next