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

# Lifecycle hooks

Lifecycle hooks let you customize workflow behavior at execution boundaries. Use hooks for logging, validation, modifying inputs/outputs, or adding context.

Workflows have two lifecycle hooks:

* `on_start` - Before workflow execution begins
* `on_end` - After workflow execution completes

<Note>
  Child workflows and agents also have their own `on_start` and `on_end` hooks.
</Note>

## Defining hooks

Create hooks using the `@hook` decorator:

<CodeGroup>
  ```python theme={null}
  from polos import hook, WorkflowContext, HookContext, HookResult

  @hook
  def log_hook(ctx: WorkflowContext, hook_context: HookContext) -> HookResult:
      """Log execution details."""
      print(f"Workflow: {hook_context.workflow_id}")
      print(f"Payload: {hook_context.current_payload}")

      return HookResult.continue_with()
  ```

  ```typescript theme={null}
  import { defineHook, HookResult } from '@polos/sdk';

  const logHook = defineHook(async (ctx, hookCtx) => {
    console.log(`Workflow: ${hookCtx.workflowId}`);
    console.log(`Payload: ${hookCtx.currentPayload}`);

    return HookResult.continue();
  }, { name: 'log-hook' });
  ```
</CodeGroup>

**Hook signature:**

* `ctx` - `WorkflowContext` with execution metadata
* `hook_context` - `HookContext` with current execution state
* Returns - `HookResult` indicating what action to take

## Hook results

Hooks return `HookResult` with three options:

### 1. Continue without changes

<CodeGroup>
  ```python theme={null}
  @hook
  def simple_hook(ctx: WorkflowContext, hook_context: HookContext) -> HookResult:
      # Just observe, don't modify
      print("Hook executed")
      return HookResult.continue_with()
  ```

  ```typescript theme={null}
  const simpleHook = defineHook(async (ctx, hookCtx) => {
    // Just observe, don't modify
    console.log('Hook executed');
    return HookResult.continue();
  }, { name: 'simple-hook' });
  ```
</CodeGroup>

### 2. Continue with modifications

<CodeGroup>
  ```python theme={null}
  import re

  @hook
  def redact_pii_hook(ctx: WorkflowContext, hook_context: HookContext) -> HookResult:
      """Redact sensitive information from payloads."""
      if not hook_context.current_payload:
          return HookResult.continue_with()

      modified = hook_context.current_payload.copy()

      # Redact emails
      if isinstance(modified, dict):
          for key, value in modified.items():
              if isinstance(value, str):
                  modified[key] = re.sub(
                      r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
                      '[EMAIL_REDACTED]',
                      value
                  )

      return HookResult.continue_with(modified_payload=modified)
  ```

  ```typescript theme={null}
  const redactPiiHook = defineHook(async (ctx, hookCtx) => {
    if (!hookCtx.currentPayload) {
      return HookResult.continue();
    }

    const modified = { ...hookCtx.currentPayload };

    // Redact emails
    for (const key of Object.keys(modified)) {
      if (typeof modified[key] === 'string') {
        modified[key] = modified[key].replace(
          /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
          '[EMAIL_REDACTED]'
        );
      }
    }

    return HookResult.continueWith({ modifiedPayload: modified });
  }, { name: 'redact-pii-hook' });
  ```
</CodeGroup>

### 3. Fail and stop execution

<CodeGroup>
  ```python theme={null}
  @hook
  def validation_hook(ctx: WorkflowContext, hook_context: HookContext) -> HookResult:
      """Validate payload before execution."""
      payload = hook_context.current_payload

      if not payload or not payload.get("required_field"):
          return HookResult.fail("Missing required field")

      return HookResult.continue_with()
  ```

  ```typescript theme={null}
  const validationHook = defineHook(async (ctx, hookCtx) => {
    const payload = hookCtx.currentPayload;

    if (!payload || !payload.requiredField) {
      return HookResult.fail('Missing required field');
    }

    return HookResult.continue();
  }, { name: 'validation-hook' });
  ```
</CodeGroup>

## Attaching hooks to workflows

<CodeGroup>
  ```python theme={null}
  from polos import workflow, WorkflowContext

  @workflow(
      id="data-processor",
      on_start=[log_hook, validate_input_hook],
      on_end=[log_hook, save_results_hook]
  )
  async def data_processor(ctx: WorkflowContext, input: dict):
      # Process data
      result = await ctx.step.run("process", process_data, input)
      return result
  ```

  ```typescript theme={null}
  import { defineWorkflow } from '@polos/sdk';

  const dataProcessor = defineWorkflow<Record<string, any>, void, any>(
    {
      id: 'data-processor',
      onStart: [logHook, validateInputHook],
      onEnd: [logHook, saveResultsHook],
    },
    async (ctx, input) => {
      const result = await ctx.step.run('process', () => processData(input));
      return result;
    }
  );
  ```
</CodeGroup>

## Hook context

Hooks receive `HookContext` with execution state:

<CodeGroup>
  ```python theme={null}
  @hook
  def inspect_context_hook(ctx: WorkflowContext, hook_context: HookContext) -> HookResult:
      # Workflow information
      workflow_id = hook_context.workflow_id

      # User context
      user_id = hook_context.user_id
      session_id = hook_context.session_id

      # Execution state
      current_payload = hook_context.current_payload
      current_output = hook_context.current_output

      # Step history (list of completed steps) - only available for agents
      steps = hook_context.steps

      return HookResult.continue_with()
  ```

  ```typescript theme={null}
  const inspectContextHook = defineHook(async (ctx, hookCtx) => {
    // Workflow information
    const workflowId = hookCtx.workflowId;

    // User context
    const userId = hookCtx.userId;
    const sessionId = hookCtx.sessionId;

    // Execution state
    const currentPayload = hookCtx.currentPayload;
    const currentOutput = hookCtx.currentOutput;

    // Step history (list of completed steps) - only available for agents
    const steps = hookCtx.steps;

    return HookResult.continue();
  }, { name: 'inspect-context-hook' });
  ```
</CodeGroup>

## Multiple hooks

Hooks run in order. If any hook fails, execution stops:

<CodeGroup>
  ```python theme={null}
  @workflow(
      id="multi-hook-workflow",
      on_start=[
          validate_input_hook,     # Runs first
          redact_pii_hook,         # Runs second (on validated input)
          add_metadata_hook        # Runs third (on redacted input)
      ]
  )
  async def multi_hook_workflow(ctx: WorkflowContext, input: dict):
      return await ctx.step.run("process", process_data, input)
  ```

  ```typescript theme={null}
  const multiHookWorkflow = defineWorkflow<Record<string, any>, void, any>(
    {
      id: 'multi-hook-workflow',
      onStart: [
        validateInputHook,   // Runs first
        redactPiiHook,       // Runs second (on validated input)
        addMetadataHook,     // Runs third (on redacted input)
      ],
    },
    async (ctx, input) => {
      return await ctx.step.run('process', () => processData(input));
    }
  );
  ```
</CodeGroup>

If `validate_input_hook` fails, `redact_pii_hook` and `add_metadata_hook` never run.
