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

# Run AI Attack Simulation red team sessions

> Use the HiddenLayer Python SDK to run AI Attack Simulation red team sessions against your own LLM application, monitor progress, and read the evaluation report.

Red teaming is the practice of attacking your own system the way an adversary would, so you find weaknesses before someone else does. For an LLM application, that means sending prompts designed to jailbreak the model, leak its system prompt, extract sensitive data, or redirect it away from its task.

HiddenLayer [AI Attack Simulation](/docs/products/ai-attack-simulation/overview) automates this. With the Python SDK, you can run a red team session against any target you can call from Python: a hosted model, an internal API, a chatbot behind a web UI, or an agent.

## How a session works

In an SDK-driven session, HiddenLayer generates the attacks and judges the results. Your code only connects the attacker to your target.

**1. You start a session**

You call `start_session` with a name, a label for your target, and settings such as the execution strategy. HiddenLayer creates a workflow and returns a session object with a `workflow_id`.

**2. HiddenLayer generates attack prompts**

HiddenLayer produces attack prompts based on the objectives and techniques you select.

**3. Your handler calls the target**

For each attack prompt, the SDK calls a function you write, called the **handler**. The handler sends the prompt to your application or model and returns the reply as a string. The SDK submits that reply back to HiddenLayer.

**4. HiddenLayer judges and reports**

HiddenLayer decides whether each attack succeeded and may continue a multi-turn conversation. When the session finishes, it produces an evaluation report that you can retrieve with the SDK or view in the Console.

Because your code calls the target, HiddenLayer never needs network access to your application or credentials for it.

> [!WARNING]
>
> All AI Attack Simulation endpoints are in beta. The SDK emits a `hiddenlayer.BetaWarning` when you call them, including through the session helpers. Beta APIs have no compatibility guarantees, and their parameters and responses can change between SDK versions. To hide the warning, call `warnings.filterwarnings("ignore", category=hiddenlayer.BetaWarning)`. See the [Beta API FAQ](https://dev.hiddenlayer.ai/docs/beta-api-faq).

## Before you begin

* Install the SDK and configure credentials. See [Installation](/docs/integrations/sdk_python/installation) and [Authentication and configuration](/docs/integrations/sdk_python/authentication).
* Set `HIDDENLAYER_CLIENT_ID` and `HIDDENLAYER_CLIENT_SECRET` in your environment, or set `HIDDENLAYER_TOKEN` to a bearer token. Every example on this page reads credentials from the environment.
* Your tenant needs access to AI Attack Simulation.
* You need a way to call your target from Python. The examples use a placeholder function so they run without extra packages. Install whatever your real target needs, such as `openai` or `httpx`.

> [!TIP]
>
> For your first run, use a small static prompt set (`execution_strategy_type="static_prompt_set"`) to confirm that your handler and target work before you start a larger adaptive evaluation. See [Execution strategies](#execution-strategies).

## Run a session with the async client

The recommended approach uses `AsyncHiddenLayer` and `run_with_callback_parallel`, which processes several attack prompts at the same time. The SDK example notebooks use this pattern.

The handler receives four arguments:

| Argument | Description |
| - | - |
| `attack_prompt` | The attack prompt to send to your target. |
| `history` | Earlier turns in this attack conversation, as a list of dicts with `role` and `content` keys. Use it if your target is stateless and needs the full conversation. |
| `session_id` | The ID of this attack conversation. Useful if your target keeps its own conversation state. |
| `target_system_prompt` | The `target_system_prompt` you passed to `start_session`. |

It must return the target's reply as a string.

```python red_team_async.py theme={null}
import asyncio
import sys
from typing import Any, Dict, List, Optional

import hiddenlayer
from hiddenlayer import AsyncHiddenLayer
from hiddenlayer.lib.red_team_exceptions import RedTeamSessionError

TARGET_MODEL = "my-support-bot"  # Free-form label for your target
TARGET_SYSTEM_PROMPT = "You are a helpful customer support assistant."


async def call_target(prompt: str, history: List[Dict[str, Any]], system_prompt: str) -> str:
    """Placeholder target. Replace the body with a call to your application or model."""
    # >>> CALL YOUR APP HERE <<<
    # For example: send `history` plus `prompt` to your chat API and return the reply text.
    await asyncio.sleep(0.1)
    return "I'm sorry, I can only help with questions about your account."


async def handler(
    attack_prompt: Optional[str],
    history: Optional[List[Dict[str, Any]]],
    session_id: Optional[str],
    target_system_prompt: Optional[str],
) -> str:
    reply = await call_target(attack_prompt or "", history or [], target_system_prompt or "")
    # Return a non-empty string. The SDK example notebooks return " " when the target replies with nothing.
    return reply or " "


async def main() -> int:
    # Reads HIDDENLAYER_CLIENT_ID and HIDDENLAYER_CLIENT_SECRET (or HIDDENLAYER_TOKEN).
    async with AsyncHiddenLayer() as client:
        workflow_id = None
        try:
            session = await client.evaluation_sessions.red_team.start_session(
                name="Support bot red team",
                target_model=TARGET_MODEL,
                target_system_prompt=TARGET_SYSTEM_PROMPT,
                execution_strategy_type="random",
                max_turns=3,
                max_parallel_techniques=3,
            )
            workflow_id = session.workflow_id
            print(f"Session started: {workflow_id}")

            await session.run_with_callback_parallel(handler)
            print("Session complete.")

            results = await client.evaluations.red_team.retrieve_evaluation_results(workflow_id)
        except RedTeamSessionError as e:
            # Includes PollTimeoutError and "Workflow failed: ..." errors.
            print(f"Red team session error: {e} (workflow: {workflow_id})", file=sys.stderr)
            return 1
        except hiddenlayer.APIStatusError as e:
            print(f"HiddenLayer API error {e.status_code}: {e.message} (workflow: {workflow_id})", file=sys.stderr)
            return 1
        except hiddenlayer.APIConnectionError as e:
            print(f"Could not reach HiddenLayer: {e} (workflow: {workflow_id})", file=sys.stderr)
            return 1

    print(f"Status: {results.status}")
    if results.result.evaluation_report:
        print("\nEvaluation report:\n")
        print(results.result.evaluation_report)

    report = results.result.report or {}
    summary = report.get("summary")
    if isinstance(summary, dict):
        print(f"\nTotal sessions: {summary.get('total_sessions')}")
        print(f"Success rate:   {summary.get('success_rate_pct')}%")
        print(f"Successful:     {summary.get('success_total')} / {summary.get('attempts_total')}")
        print(f"Errors:         {summary.get('error_total')}")
    return 0


if __name__ == "__main__":
    sys.exit(asyncio.run(main()))
```

**Expected result:** the script prints the workflow ID right away, then runs until HiddenLayer finishes the session. This can take several minutes. It then prints the final status, the text evaluation report, and summary counts. The session and its report also appear in the Console.

> [!TIP]
>
> Print or log the `workflow_id` as soon as the session starts. If your script stops, you need it to [resume](#resume-a-session), [check status](#monitor-a-session), or [terminate](#terminate-a-session) the session.

> **Variant: target an OpenAI model**
>
> This handler sends each attack prompt to an OpenAI chat model. It requires `pip install openai` and an `OPENAI_API_KEY` environment variable. It follows the SDK's `red_team_openai.ipynb` notebook.
>
> ```python red_team_openai_handler.py theme={null}
> import os
> from typing import Any, Dict, List, Optional
>
> from openai import AsyncOpenAI
>
> openai_client = AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"])
> TARGET_MODEL = "gpt-4o-mini"
> TARGET_SYSTEM_PROMPT = "You are a helpful AI assistant."
>
>
> async def handler(
>     attack_prompt: Optional[str],
>     history: Optional[List[Dict[str, Any]]],
>     session_id: Optional[str],
>     target_system_prompt: Optional[str],
> ) -> str:
>     messages: List[Dict[str, Any]] = [{"role": "system", "content": TARGET_SYSTEM_PROMPT}]
>     messages.extend(history or [])
>     messages.append({"role": "user", "content": attack_prompt or ""})
>     response = await openai_client.chat.completions.create(model=TARGET_MODEL, messages=messages)
>     return response.choices[0].message.content or " "
> ```
>
> Use this `handler` in place of the placeholder in the main example, and pass `target_model=TARGET_MODEL` and `target_system_prompt=TARGET_SYSTEM_PROMPT` to `start_session`.

### Control parallelism

`run_with_callback_parallel(handler, max_parallel=None, poll_batch_interval=0.5)` runs up to `max_parallel` handler calls at once. If you leave `max_parallel` as `None`, it uses the session's `max_parallel_techniques`. Set it lower if your target has rate limits.

If any handler call raises an exception, the SDK cancels the remaining in-flight calls and re-raises the exception. Handle expected target errors inside your handler if you want the session to continue.

## start\_session parameters

`client.evaluation_sessions.red_team.start_session(...)` accepts these keyword arguments. Defaults are the SDK defaults. Where the SDK sends nothing, the HiddenLayer service applies its own default.

| Parameter | Default | Description |
| - | - | - |
| `name` | Required | Name of the evaluation, shown in the Console. |
| `target_model` | Required | Free-form label for your target. Because your code calls the target, HiddenLayer does not validate this value against a model catalog. |
| `target_system_prompt` | `""` | Your target's system prompt. It gives the attacker context and is passed to your handler. |
| `objective_ids` | Not sent | List of objective IDs to test, for example `["HLO.01", "HLO.03"]`. See [Basic custom attack](/docs/products/ai-attack-simulation/custom-attack-sim/basic-custom-attack) for objective IDs and names. |
| `execution_strategy_type` | `"random"` | `"single"`, `"random"`, or `"static_prompt_set"`. Any other value raises `ValueError`. |
| `prompt_set_id` | Not sent | ID of a prompt set. Required when `execution_strategy_type="static_prompt_set"`. The SDK does not check this for you. |
| `max_turns` | `3` | Maximum conversation turns per attack session. Applies to `single` and `random`. |
| `n_random_techniques` | `1` | Number of random techniques to use with the `random` strategy. |
| `max_parallel_techniques` | `1` | Number of techniques to run in parallel. Also the default `max_parallel` for `run_with_callback_parallel`. |
| `sessions_per_technique` | `1` | Number of attack sessions per technique. With a static prompt set, the number of attempts per prompt. |
| `attacker_guidance` | Not sent | Plain-language text that focuses the attacker within your objectives. HiddenLayer rejects text that contains code, markup, or other disallowed characters with a 422 error. Ignored by `static_prompt_set`. |
| `severity_mapping` | Not sent | Dict that maps objective IDs to `"CRITICAL"`, `"HIGH"`, `"MEDIUM"`, `"LOW"`, or `"NONE"`. Used to derive each session's severity. |
| `config_id` | Not sent | ID of a preset configuration. Any parameter you also pass overrides the preset value. |
| `hiddenlayer_project_id` | `None` | HiddenLayer project ID or alias to associate the session with. |
| `attacker_model`, `refusal_judge_model`, `objective_judge_model`, `evaluation_report_model` | Not sent | Overrides for the models HiddenLayer uses internally. The service default applies if omitted. |
| `attacker_max_generation_attempts` | `2` | Maximum attempts for the attacker to generate a prompt per turn. |
| `poll_interval` | `2.0` | Seconds between checks for the next attack prompt. |
| `poll_max_wait` | `None` | Overall time limit, in seconds, for polling for attack prompts. See [Timeouts](#timeouts). |

## Read the results

After the session completes, call `client.evaluations.red_team.retrieve_evaluation_results(workflow_id)`. The response has `workflow_id`, `run_id`, `status`, and a `result` object with these fields:

| Field | Type | Description |
| - | - | - |
| `result.evaluation_report` | `str` or `None` | A text evaluation report. |
| `result.report` | `dict` or `None` | Structured results, such as summary counts and per-objective results. |
| `result.settings` | `dict` or `None` | The settings the session ran with. |
| `result.usage` | `dict` or `None` | Usage information. |
| `result.attacker_results` | `dict` or `None` | Attacker results. |

> [!NOTE]
>
> The SDK types `result.report` as a plain `dict`, so its keys are not guaranteed. The keys used below come from the SDK example notebooks: `report["summary"]` with `total_sessions`, `success_rate_pct`, `success_total`, `attempts_total`, and `error_total`, and `report["by_objective"]`, which maps each objective ID to a dict with `success` and `attempts`. Use `.get()` and check types so your code keeps working if the structure changes.

```python print_results.py theme={null}
import asyncio
import sys

import hiddenlayer
from hiddenlayer import AsyncHiddenLayer


async def main(workflow_id: str) -> None:
    async with AsyncHiddenLayer() as client:
        try:
            results = await client.evaluations.red_team.retrieve_evaluation_results(workflow_id)
        except hiddenlayer.NotFoundError:
            sys.exit(f"No workflow found with ID {workflow_id}. Check the ID and environment (region).")
        except hiddenlayer.APIStatusError as e:
            sys.exit(f"HiddenLayer API error {e.status_code}: {e.message}")

    report = results.result.report or {}
    by_objective = report.get("by_objective")
    if isinstance(by_objective, dict):
        for objective_id, stats in by_objective.items():
            if not isinstance(stats, dict):
                continue
            success = stats.get("success", 0)
            attempts = stats.get("attempts", 0)
            outcome = "PASS" if success == 0 else "FAIL"
            print(f"{objective_id}: {success}/{attempts} attacks succeeded [{outcome}]")


if __name__ == "__main__":
    asyncio.run(main(sys.argv[1]))
```

Run it with the workflow ID as an argument, for example `python print_results.py <workflow_id>`. An objective is marked `FAIL` if at least one attack against it succeeded.

## Run a session with the sync client

If your code is not asynchronous, use `HiddenLayer` and `run_with_callback`. The handler is a regular function. The sync session processes one attack prompt at a time, so it is slower than the parallel async approach.

```python red_team_sync.py theme={null}
import sys
from typing import Any, Dict, List, Optional

import hiddenlayer
from hiddenlayer import HiddenLayer
from hiddenlayer.lib.red_team_exceptions import RedTeamSessionError


def call_target(prompt: str, history: List[Dict[str, Any]], system_prompt: str) -> str:
    """Placeholder target. Replace the body with a call to your application or model."""
    # >>> CALL YOUR APP HERE <<<
    return "I'm sorry, I can't help with that."


def handler(
    attack_prompt: Optional[str],
    history: Optional[List[Dict[str, Any]]],
    session_id: Optional[str],
    target_system_prompt: Optional[str],
) -> str:
    return call_target(attack_prompt or "", history or [], target_system_prompt or "") or " "


with HiddenLayer() as client:
    try:
        session = client.evaluation_sessions.red_team.start_session(
            name="Support bot red team (sync)",
            target_model="my-support-bot",
            target_system_prompt="You are a helpful customer support assistant.",
            execution_strategy_type="random",
        )
        print(f"Session started: {session.workflow_id}")

        session.run_with_callback(handler)

        results = client.evaluations.red_team.retrieve_evaluation_results(session.workflow_id)
    except RedTeamSessionError as e:
        sys.exit(f"Red team session error: {e}")
    except hiddenlayer.APIStatusError as e:
        sys.exit(f"HiddenLayer API error {e.status_code}: {e.message}")
    except hiddenlayer.APIConnectionError as e:
        sys.exit(f"Could not reach HiddenLayer: {e}")

print(results.result.evaluation_report)
```

**Expected result:** the same as the async example, but attack prompts are handled one at a time.

Sessions also expose lower-level methods if you need more control: `iterate_actions()` yields each ready attack action, and `get_next_action(block=True)` returns the next one. The async session also provides `get_available_actions(max_actions=10)`. If you use these, you must submit each target reply yourself with `client.evaluations.red_team.submit_target_response(workflow_id=..., session_id=..., target_response=...)`. `run_with_callback` and `run_with_callback_parallel` do this for you.

## Execution strategies

Set `execution_strategy_type` to choose how HiddenLayer builds attacks:

| Strategy | What it does | Related parameters |
| - | - | - |
| `"random"` (default) | AI-generated, adaptive attacks that mix in additional randomly selected techniques per session for variety. | `max_turns`, `n_random_techniques` |
| `"single"` | AI-generated, adaptive attacks without the random technique mixing. | `max_turns` |
| `"static_prompt_set"` | Replays a fixed set of prompts. Useful for first-time setup checks, regression testing, and benchmarking. | `prompt_set_id` (required), `sessions_per_technique` |

> [!NOTE]
>
> The exact behavioral difference between `single` and `random` is not documented in the SDK. The descriptions above follow the existing [usage examples](/docs/products/ai-attack-simulation/custom-attack-sim/usage-examples) and need confirmation.

For `static_prompt_set`, pass a prompt set ID. The SDK example notebook mentions these built-in sets: `do-anything-now-ape`, `financial-assistant-basic`, and `financial-assistant-advanced`. You can also copy the ID of a prompt set you uploaded from the Console.

```python theme={null}
# Inside an async function, where `client` is an AsyncHiddenLayer instance:
session = await client.evaluation_sessions.red_team.start_session(
    name="Static prompt regression test",
    target_model="my-support-bot",
    execution_strategy_type="static_prompt_set",
    prompt_set_id="do-anything-now-ape",
)
```

## Monitor a session

Call `client.evaluations.red_team.retrieve_status(workflow_id)` to check progress, for example from a separate process or dashboard. Useful fields include:

| Field | Description |
| - | - |
| `status` | Workflow status, for example `RUNNING`. Terminal statuses are `COMPLETED`, `FAILED`, `CANCELLED`, `TERMINATED`, and `TIMED_OUT`. |
| `phase` | Current phase of the workflow. |
| `progress_completed`, `progress_total` | Progress counters. |
| `total_sessions`, `active_sessions`, `completed_sessions`, `failed_sessions` | Attack session counts. |
| `elapsed_seconds`, `eta_seconds` | Elapsed time and estimated time remaining. |
| `error`, `message` | Error details and status messages, when present. |

```python monitor.py theme={null}
import sys

import hiddenlayer
from hiddenlayer import HiddenLayer

workflow_id = sys.argv[1]

with HiddenLayer() as client:
    try:
        status = client.evaluations.red_team.retrieve_status(workflow_id)
    except hiddenlayer.NotFoundError:
        sys.exit(f"No workflow found with ID {workflow_id}.")
    except hiddenlayer.APIStatusError as e:
        sys.exit(f"HiddenLayer API error {e.status_code}: {e.message}")

print(f"{status.name}: {status.status} (phase: {status.phase})")
print(f"Progress: {status.progress_completed}/{status.progress_total}")
if status.error:
    print(f"Error: {status.error}")
```

## Resume a session

If your script stops while a session is running, the workflow continues to exist on the HiddenLayer side. Reconnect to it with `resume_session(workflow_id, *, poll_interval=2.0, poll_max_wait=None, max_parallel_techniques=None)`, then drive it again with your handler. If you do not pass `max_parallel_techniques`, the resumed session uses `1`.

```python resume.py theme={null}
import asyncio
import sys
from typing import Any, Dict, List, Optional

import hiddenlayer
from hiddenlayer import AsyncHiddenLayer
from hiddenlayer.lib.red_team_exceptions import RedTeamSessionError


async def handler(
    attack_prompt: Optional[str],
    history: Optional[List[Dict[str, Any]]],
    session_id: Optional[str],
    target_system_prompt: Optional[str],
) -> str:
    # >>> CALL YOUR APP HERE <<< (use the same handler as the original run)
    return "I'm sorry, I can't help with that."


async def main(workflow_id: str) -> None:
    async with AsyncHiddenLayer() as client:
        try:
            session = await client.evaluation_sessions.red_team.resume_session(
                workflow_id, max_parallel_techniques=3
            )
            status = await client.evaluations.red_team.retrieve_status(workflow_id)
            print(f"Resumed {session.workflow_id}: {status.status}")

            if status.status == "RUNNING":
                await session.run_with_callback_parallel(handler)

            results = await client.evaluations.red_team.retrieve_evaluation_results(workflow_id)
        except (RedTeamSessionError, hiddenlayer.APIStatusError) as e:
            sys.exit(f"Could not resume workflow {workflow_id}: {e}")

    print(results.result.evaluation_report)


if __name__ == "__main__":
    asyncio.run(main(sys.argv[1]))
```

## Terminate a session

To stop a running session, call `terminate_session(workflow_id)`.

> [!WARNING]
>
> Terminating a session is irreversible. The workflow stops and cannot be restarted. Confirm the workflow ID before you run this.

```python terminate.py theme={null}
import sys

import hiddenlayer
from hiddenlayer import HiddenLayer
from hiddenlayer.lib.red_team_exceptions import RedTeamSessionError

workflow_id = sys.argv[1]

with HiddenLayer() as client:
    try:
        client.evaluation_sessions.red_team.terminate_session(workflow_id)
    except (RedTeamSessionError, hiddenlayer.APIStatusError) as e:
        sys.exit(f"Could not terminate workflow {workflow_id}: {e}")

print(f"Terminated {workflow_id}")
```

The async client provides the same method: `await client.evaluation_sessions.red_team.terminate_session(workflow_id)`.

## Timeouts

Red team sessions can run for a long time. Two separate limits apply:

* **`poll_max_wait`** (on `start_session` and `resume_session`, default `None`): an overall time limit, in seconds, measured from when the session object is created. The SDK checks it while it polls for attack prompts. When exceeded, it raises a plain `Exception` with a message like `Polling timed out after ...`, **not** `PollTimeoutError`. With the default `None`, there is no limit.
* **`wait_for_completion`**: after all attack prompts are handled, `run_with_callback` and `run_with_callback_parallel` call `wait_for_completion()` with its defaults (`poll_interval=3.0`, `max_wait=300.0`). If the workflow does not reach a terminal status within 300 seconds, it raises `PollTimeoutError`. If the workflow status is `FAILED`, it raises `RedTeamSessionError` with the message `Workflow failed: ...`.

A client-side timeout does not stop the workflow on the HiddenLayer side. After a timeout, check the status with `retrieve_status`, fetch results later, or resume the session. You can also call `session.wait_for_completion(max_wait=...)` yourself with a longer limit, or `max_wait=None` for no limit.

```python theme={null}
from hiddenlayer.lib.red_team_exceptions import PollTimeoutError


async def run_with_timeouts(session, handler) -> None:
    """`session` comes from start_session or resume_session on an AsyncHiddenLayer client."""
    try:
        await session.run_with_callback_parallel(handler)
    except PollTimeoutError:
        # All prompts were handled, but the workflow is still finalizing. Wait longer.
        await session.wait_for_completion(max_wait=900.0)
    except Exception as e:
        if str(e).startswith("Polling timed out"):
            print(f"poll_max_wait exceeded; workflow {session.workflow_id} may still be running.")
        else:
            raise
```

## Error handling

Import the session helper exceptions from `hiddenlayer.lib.red_team_exceptions`:

| Exception | Raised when |
| - | - |
| `RedTeamSessionError` | Base class for the exceptions below. Also raised directly when the workflow fails. |
| `PollTimeoutError` | `wait_for_completion` exceeds `max_wait`. |
| `WorkflowNotFoundError` | Intended for a workflow ID that does not exist. |
| `InvalidSessionError` | Intended for an invalid attack session ID when submitting a target response. |

> [!WARNING]
>
> The session helpers map HTTP errors to `WorkflowNotFoundError` and `InvalidSessionError` by catching `httpx.HTTPStatusError`. However, the SDK client raises `hiddenlayer.APIStatusError` subclasses, such as `hiddenlayer.NotFoundError`, rather than `httpx.HTTPStatusError`. In practice you may receive `hiddenlayer.NotFoundError` or `hiddenlayer.BadRequestError` instead. This behavior needs confirmation. Until then, catch both `RedTeamSessionError` and `hiddenlayer.APIStatusError`, as the examples on this page do.

Also handle errors from your own target inside the handler. An unhandled exception in the handler stops `run_with_callback_parallel` and cancels other in-flight handler calls.

See [Error handling](/docs/integrations/sdk_python/error_handling) for the full SDK exception hierarchy.

## Common errors and fixes

> **ValueError: execution\_strategy\_type must be 'single', 'random', or 'static\_prompt\_set'.**
>
> You passed an unsupported strategy name. Use one of the three lowercase values.

> **The static\_prompt\_set session fails or runs no prompts**
>
> Check that you passed `prompt_set_id`. The SDK does not validate that it is present. Confirm the ID in the Console.

> **UnprocessableEntityError (422) when starting a session**
>
> A parameter failed validation. A common cause is `attacker_guidance` that contains code, JSON, XML, markdown, or other characters outside plain sentence punctuation. Another is a `severity_mapping` key that is not a known objective ID. Print `e.body` for details.

> **PollTimeoutError: Workflow did not complete within 300.0s**
>
> Your handler finished, but the workflow took longer than five minutes to finalize. The workflow is still running. Call `session.wait_for_completion(max_wait=...)` with a longer limit, or check `retrieve_status` later and then fetch the results.

> **Exception: Polling timed out after ...**
>
> The `poll_max_wait` limit you set was exceeded. Increase it, or leave it as `None`. Resume the session with `resume_session` to continue.

> **RedTeamSessionError: Workflow failed: ...**
>
> HiddenLayer reported the workflow as `FAILED`. The message includes the error from the workflow status. Check that your handler returns meaningful strings and that your target is reachable, then start a new session.

> **NotFoundError (404) for a workflow ID**
>
> The workflow ID is wrong, or you are connected to a different environment (region) than the one that created it.

> **AuthenticationError (401) or PermissionDeniedError (403)**
>
> Check your credentials and environment, and confirm that your tenant has access to AI Attack Simulation. See [Authentication and configuration](/docs/integrations/sdk_python/authentication).

## More examples

The SDK repository includes [example notebooks](https://github.com/hiddenlayerai/hiddenlayer-sdk-python/tree/main/examples/red_team) for different kinds of targets: an OpenAI model, a custom HTTP API, a web chat UI driven by Playwright, a static prompt set, a webhook-based target, a WebSocket target, and a generic template.

For more background and Console-based workflows, see:

* [AI Attack Simulation overview](/docs/products/ai-attack-simulation/overview)
* [Basic custom attack](/docs/products/ai-attack-simulation/custom-attack-sim/basic-custom-attack)
* [Custom attack usage examples](/docs/products/ai-attack-simulation/custom-attack-sim/usage-examples)
