Queuety
Examples

Neuron Session State Machine

This pattern is useful when the thing you are building behaves more like a session than a one-off run.

Queuety state machines own the session lifecycle, Queuety workflows do the background work that benefits from retries, fan-out, and joins, and Neuron handles the actual agent calls and conversation memory.

That split is a good fit for support copilots, internal research assistants, editorial agents that may pause for human review, and chat threads that move between planning, waiting, and response phases.

Lifecycle shape

In this example, one support session moves through awaiting_user, planning, waiting_on_research, and responding. The machine keeps that lifecycle explicit, while a background workflow gathers research and signals the session when it is done.

The state machine

use Queuety\Queuety;

$session_id = Queuety::machine( 'support_session' )
    ->version( 'support-session.v1' )
    ->state( 'awaiting_user' )
    ->on( 'user_message', 'planning' )
    ->state( 'planning' )
    ->action( DispatchResearchWorkflowAction::class )
    ->on( 'research_dispatched', 'waiting_on_research' )
    ->state( 'waiting_on_research' )
    ->on( 'research_completed', 'responding' )
    ->state( 'responding' )
    ->action( RespondWithNeuronAction::class )
    ->on( 'response_ready', 'awaiting_user' )
    ->dispatch( [
        'thread_id' => 'support:customer-42',
    ] );

When a user sends a message:

Queuety::machine_event(
    $session_id,
    'user_message',
    [
        'session_machine_id' => $session_id,
        'message' => 'Can you compare our pricing to the main competitors?',
    ]
);

The machine transitions into planning, queues its action, and starts the research workflow.

Planning action: dispatch the workflow

namespace App\StateMachines\Actions;

use Queuety\Contracts\StateAction;
use Queuety\Enums\WaitMode;
use Queuety\Queuety;

final class DispatchResearchWorkflowAction implements StateAction
{
    public function handle( array $state, ?string $event = null, array $event_payload = [] ): array|string
    {
        $message = trim( (string) ( $event_payload['message'] ?? '' ) );

        $agent_workflow = Queuety::workflow( 'support_research_agent' )
            ->then( ResearchTopicStep::class );

        $research_workflow = Queuety::workflow( 'support_research_packet' )
            ->then( PlanResearchTasksStep::class )
            ->spawn_agents( 'agent_tasks', $agent_workflow )
            ->await_agents( mode: WaitMode::All, result_key: 'agent_results' )
            ->then( SynthesizeResearchPacketStep::class )
            ->then( NotifySessionResearchReadyStep::class );

        $workflow_id = $research_workflow->dispatch( [
            'machine_id'      => $state['session_machine_id'],
            'thread_id'       => $state['thread_id'],
            'customer_prompt' => $message,
        ] );

        return [
            'latest_user_message'  => $message,
            'research_workflow_id' => $workflow_id,
            '_event'               => 'research_dispatched',
        ];
    }
}

This state action does not try to do the research itself. It dispatches a workflow and records the workflow ID in machine state.

Workflow step: signal the machine when research is ready

namespace App\Workflow\Steps;

use Queuety\Queuety;
use Queuety\Step;

final class NotifySessionResearchReadyStep implements Step
{
    public function handle( array $state ): array
    {
        Queuety::machine_event(
            $state['machine_id'],
            'research_completed',
            [
                'research_packet' => $state['research_packet'],
            ]
        );

        return [];
    }

    public function config(): array
    {
        return [];
    }
}

When the workflow finishes, it sends research_completed back into the machine and includes the synthesized packet as event payload.

Responding action: call Neuron with session memory

namespace App\StateMachines\Actions;

use App\Neuron\SupportAgent;
use NeuronAI\Chat\Messages\UserMessage;
use Queuety\Contracts\StateAction;

final class RespondWithNeuronAction implements StateAction
{
    public function __construct( private readonly \PDO $pdo ) {}

    public function handle( array $state, ?string $event = null, array $event_payload = [] ): array|string
    {
        $agent = new SupportAgent(
            threadId: $state['thread_id'],
            pdo: $this->pdo,
        );

        $message = $agent->chat(
            new UserMessage(
                sprintf(
                    "Answer the user message '%s' using this research packet:\n\n%s",
                    $state['latest_user_message'],
                    json_encode( $state['research_packet'] ?? [], JSON_PRETTY_PRINT )
                )
            )
        )->getMessage();

        return [
            'assistant_reply' => $message->getContent(),
            '_event'          => 'response_ready',
        ];
    }
}

After the response is generated, the machine transitions back to awaiting_user with the updated reply in public state. The same session can then accept another user_message event and continue.

Why this is cleaner than one large workflow

The heavy lifting is still workflow-shaped because the research side needs planning, specialist runs, joins, and synthesis. The long-lived support conversation is state-machine-shaped because the session must keep track of whether it is waiting for a user turn, waiting on research, or allowed to emit a response.

That split keeps the runtime readable and keeps the session model explicit.

On this page