> ## 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.

# Cancelling workflows

Stop workflows to prevent runaway executions and free up resources. Workflows can be cancelled via timeout or manual cancellation.

## Timeout-based cancellation

Set `run_timeout_seconds` to automatically cancel workflows that run too long:

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

  @workflow
  async def risky_workflow(ctx: WorkflowContext, input: dict):
      # Potentially long-running workflow

  # Invoke with timeout
  client = PolosClient()
  handle = await risky_workflow.invoke(
      client,
      payload={"data": "..."},
      run_timeout_seconds=1800  # Cancel after 30 minutes
  )
  ```

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

  const riskyWorkflow = defineWorkflow<Record<string, string>, void, void>(
    { id: 'risky-workflow' },
    async (ctx, input) => {
      // Potentially long-running workflow
    }
  );

  // Invoke with timeout
  const client = PolosClient.fromEnv();
  const handle = await riskyWorkflow.invoke(
    client,
    { data: '...' },
    { runTimeoutSeconds: 1800 } // Cancel after 30 minutes
  );
  ```
</CodeGroup>

**How timeouts work:**

* Only applies to running workflows (actively executing)
* Does not apply to waiting workflows (paused on `wait_for`, `wait_for_event`, etc.)
* Orchestrator and/or worker automatically cancels execution when timeout is reached
* Workflow stops immediately

**Example with waits:**

<CodeGroup>
  ```python theme={null}
  @workflow
  async def workflow_with_waits(ctx: WorkflowContext, input: dict):
      # Step 1: Process (counts toward timeout)
      await ctx.step.run("process", process_data, input)

      # Step 2: Wait 1 hour (does NOT count toward timeout)
      await ctx.step.wait_for("wait", hours=1)

      # Step 3: More processing (counts toward timeout, but resets execution time of step 1)
      await ctx.step.run("finalize", finalize_data, input)

  # Timeout only counts running time, not waiting time
  client = PolosClient()
  handle = await workflow_with_waits.invoke(
      client,
      payload={...},
      run_timeout_seconds=60  # 60 seconds of continuous running time
  )
  ```

  ```typescript theme={null}
  const workflowWithWaits = defineWorkflow<Record<string, any>, void, void>(
    { id: 'workflow-with-waits' },
    async (ctx, input) => {
      // Step 1: Process (counts toward timeout)
      await ctx.step.run('process', () => processData(input));

      // Step 2: Wait 1 hour (does NOT count toward timeout)
      await ctx.step.waitFor('wait', { hours: 1 });

      // Step 3: More processing (counts toward timeout, but resets execution time of step 1)
      await ctx.step.run('finalize', () => finalizeData(input));
    }
  );

  // Timeout only counts running time, not waiting time
  const client = PolosClient.fromEnv();
  const handle = await workflowWithWaits.invoke(
    client,
    { /* ... */ },
    { runTimeoutSeconds: 60 } // 60 seconds of continuous running time
  );
  ```
</CodeGroup>

**Run timeout vs total duration:**

* `run_timeout_seconds=60` → Workflow can run for 60 seconds of active execution
* If workflow waits for 24 hours, that time does not count toward timeout
* Total workflow duration can exceed `run_timeout_seconds` if it includes waits

## Manual cancellation

Cancel workflows manually using the SDK or API

<CodeGroup>
  ```python theme={null}
  from polos import PolosClient

  client = PolosClient()

  # Start workflow
  handle = await risky_workflow.invoke(client, {"data": "..."})

  # Cancel manually
  cancelled = await client.cancel_execution(handle.id)

  if cancelled:
      print("Workflow cancelled successfully")
  else:
      print("Workflow already completed or not found")
  ```

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

  const client = PolosClient.fromEnv();

  // Start workflow
  const handle = await riskyWorkflow.invoke(client, { data: '...' });

  // Cancel manually
  const cancelled = await client.cancelExecution(handle.id);

  if (cancelled) {
    console.log('Workflow cancelled successfully');
  } else {
    console.log('Workflow already completed or not found');
  }
  ```
</CodeGroup>

**Or via the handle:**

<CodeGroup>
  ```python theme={null}
  handle = await risky_workflow.invoke(client, {"data": "..."})

  # Cancel using handle
  cancelled = await handle.cancel(client)
  ```

  ```typescript theme={null}
  const handle = await riskyWorkflow.invoke(client, { data: '...' });

  // Cancel using handle
  const cancelled = await handle.cancel(client);
  ```
</CodeGroup>

**Or via API:**

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

  async def cancel_workflow_api(execution_id: str, api_key: str):
      """Cancel workflow via API."""
      async with httpx.AsyncClient() as client:
          response = await client.post(
              f"https://api.polos.ai/api/v1/executions/{execution_id}/cancel",
              headers={
                  "Authorization": f"Bearer {api_key}",
                  "Content-Type": "application/json"
              }
          )

          if response.status_code == 200:
              return True
          elif response.status_code == 404:
              print(f"Execution not found: {execution_id}")
              return False
          else:
              response.raise_for_status()
  ```

  ```typescript theme={null}
  async function cancelWorkflowApi(executionId: string, apiKey: string): Promise<boolean> {
    const response = await fetch(
      `https://api.polos.ai/api/v1/executions/${executionId}/cancel`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json',
        },
      }
    );

    if (response.status === 200) {
      return true;
    } else if (response.status === 404) {
      console.log(`Execution not found: ${executionId}`);
      return false;
    } else {
      throw new Error(`Request failed with status ${response.status}`);
    }
  }
  ```
</CodeGroup>

### What can be cancelled

* ✅ Running, queued or waiting workflows can be cancelled
* ❌ Completed or failed workflows cannot be cancelled

### How does it work?

When a workflow is cancelled:

1. Orchestrator issues cancel signal to the worker
2. Worker stops execution immediately
3. Workflow status changes to "cancelled"
4. No more steps execute

**Current step behavior:**

* If a step is running when cancel is issued, it may complete before stopping
* Steps are not interrupted mid-execution
* Cancellation takes effect between steps
