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

# JOB_COMPLETED Hook

> React to background job completion — success or failure — with a lightweight event hook

`HookEvent.JOB_COMPLETED` fires from `BackgroundJobManager.start_job` whenever any managed background job reaches a terminal state — completed or failed. Register a hook to log completions, send alerts, or trigger follow-up work.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(name="Coordinator", instructions="Delegate long tasks to background workers")
agent.start("Analyse this repo and email me when done")
```

The user starts background work; your `JOB_COMPLETED` hook runs when the subagent finishes or fails.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "JOB_COMPLETED Hook Flow"
        J[⚙️ Background job ends] --> E[📣 HookEvent.JOB_COMPLETED]
        E --> H[🔧 Your hook function]
        H -->|log / alert / chain| OK[✅ Continue]
    end

    classDef trigger fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef event fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef hook fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff

    class J trigger
    class E event
    class H hook
    class OK ok
```

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Agent
    participant Job as Background Job
    participant Hook as JOB_COMPLETED Hook

    User->>Agent: Start background task
    Agent->>Job: Spawn subagent (background=True)
    Job-->>Hook: Job reaches terminal state
    Hook->>Hook: Log / alert / chain follow-up
    Hook-->>Agent: HookResult(decision="allow")
    Agent-->>User: Notified of completion
```

## Quick Start

<Steps>
  <Step title="Create a hook registry">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult
    from praisonaiagents.hooks import JobCompletedInput

    registry = HookRegistry()
    ```
  </Step>

  <Step title="Register a JOB_COMPLETED handler">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    @registry.on(HookEvent.JOB_COMPLETED)
    def log_completion(data: JobCompletedInput) -> HookResult:
        with open("job_completions.log", "a") as f:
            f.write(
                f"{data.job_id}\t{data.status}\t{data.deliver or '-'}\t{data.error or ''}\n"
            )
        return HookResult(decision="allow")
    ```
  </Step>

  <Step title="Attach to your agent">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent

    agent = Agent(
        name="SupportBot",
        instructions="Help users.",
        hooks=registry,
    )
    ```
  </Step>
</Steps>

***

## How It Works

The user delegates work to a background job; when the job ends, the hook fires with the outcome.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Agent
    participant Job as Background Job
    participant Hook as JOB_COMPLETED Hook

    User->>Agent: "Analyse this repo and email me when done"
    Agent->>Job: spawn_subagent(background=True)
    Agent-->>User: Job started (job_id)
    Job->>Job: Run work
    Job->>Hook: JobCompletedInput (status, result, error)
    Hook-->>User: Log / alert / follow-up
```

***

## `JobCompletedInput` Fields

`JobCompletedInput` carries everything about the finished job, including the delivery context if one was set. All origin fields are strings — empty (`""`) when not set.

| Field | Type | Description |
| - | - | - |
| `job_id` | `str` | Unique job identifier |
| `status` | `str` | Terminal state: `"completed"` or `"failed"` |
| `result` | `Any` | The job's return value on success; `None` on failure |
| `error` | `str \| None` | Error message on failure; `None` on success |
| `agent_name` | `str` | Always `"background"` for jobs the manager runs |
| `session_id` | `str` | Origin session captured at spawn time (empty when unset) |
| `deliver` | `str` | Delivery target set at spawn time (e.g. `"origin"`, `"telegram:123"`, or `""`) |
| `platform` | `str` | Origin platform captured at spawn time |
| `chat_id` | `str` | Origin chat/channel ID captured at spawn time |
| `thread_id` | `str` | Origin thread ID captured at spawn time |

Origin fields (`session_id`, `deliver`, `platform`, `chat_id`, `thread_id`) are always strings — an empty string when not set.

***

## Behaviour Notes

* **Fires for every managed background job**, not only those launched via `spawn_subagent(background=True)` — `BackgroundJobManager.start_job` emits it directly at the terminal path.
* **Fires on both success and failure.** Check `data.status` (`"completed"` or `"failed"`) to distinguish.
* **Does not fire for `CANCELLED` or `LOST`.** The enum documents "finished ok or error"; that contract was deliberately not widened.
* **Best-effort.** A raising hook never crashes the background worker — exceptions are caught and logged.
* **Sits alongside `SCHEDULE_TRIGGER`.** Both fire in the background-job lifecycle and follow the same hook shape.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
@registry.on(HookEvent.JOB_COMPLETED)
def alert_on_failure(data: JobCompletedInput) -> HookResult:
    if data.status == "failed":
        send_alert(f"Job {data.job_id} failed: {data.error}")
    return HookResult(decision="allow")
```

***

## Logging Example

The shortest copy-paste hook that logs every completion to a file:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import datetime
from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult
from praisonaiagents.hooks import JobCompletedInput

registry = HookRegistry()

@registry.on(HookEvent.JOB_COMPLETED)
def log_job(data: JobCompletedInput) -> HookResult:
    ts = datetime.datetime.utcnow().isoformat()
    line = f"[{ts}] job={data.job_id} status={data.status} deliver={data.deliver!r}\n"
    if data.error:
        line += f"  error: {data.error}\n"
    with open("jobs.log", "a") as f:
        f.write(line)
    return HookResult(decision="allow")
```

***

## Best Practices

<AccordionGroup>
  <Accordion title="Make hooks idempotent">
    Background jobs may be retried or replayed. Design your `JOB_COMPLETED` hook so running it twice for the same `job_id` produces no unintended side effects — for example, check if a log entry already exists before appending, or use an upsert rather than an insert.
  </Accordion>

  <Accordion title="Do not block the event loop">
    The hook dispatcher is synchronous by default. Avoid slow I/O (database writes, HTTP calls) inside the hook body — offload to a thread pool or enqueue to a background queue instead:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    import concurrent.futures

    _pool = concurrent.futures.ThreadPoolExecutor(max_workers=4)

    @registry.on(HookEvent.JOB_COMPLETED)
    def notify_async(data: JobCompletedInput) -> HookResult:
        _pool.submit(send_slack_message, data.job_id, data.status)
        return HookResult(decision="allow")
    ```
  </Accordion>

  <Accordion title="Errors in hooks are logged, not raised">
    Per the hook dispatcher in `praisonaiagents/hooks/runner.py`, exceptions raised inside a hook are caught and logged — they never crash the background worker. Use this as a safety net, but do not rely on it: log errors explicitly so failures are visible.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    @registry.on(HookEvent.JOB_COMPLETED)
    def safe_hook(data: JobCompletedInput) -> HookResult:
        try:
            record_to_db(data)
        except Exception as exc:
            import logging
            logging.getLogger(__name__).error("hook failed: %s", exc)
        return HookResult(decision="allow")
    ```
  </Accordion>

  <Accordion title="Chain follow-up work via deliver=, not inline">
    If a completed job should trigger another agent, use the `deliver` field to route the result back through the gateway — do not call a second agent inline from the hook. Inline calls block the dispatcher and can create nested background jobs that are harder to trace.
  </Accordion>

  <Accordion title="Check status before acting">
    Use `data.status` to distinguish success from failure. Only trigger alerts or follow-up workflows when `status == "failed"` to avoid noise from normal completions.
  </Accordion>

  <Accordion title="My deliver field is '' — is that a bug?">
    No. `deliver` is empty for background jobs the caller did not route anywhere. The event still fires for every managed job; an empty `deliver` just means there is no delivery target attached.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Subagent Tool" icon="robot" href="/docs/features/subagent-tool">
    Spawn background subagents with `deliver=` routing
  </Card>

  <Card title="Hook Events" icon="bolt" href="/docs/features/hook-events">
    Full hook system reference — all events and patterns
  </Card>

  <Card title="fire_hook" icon="megaphone" href="/docs/features/fire-hook">
    How lifecycle events are emitted to subscribers
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.