# Gates

Gate nodes control execution flow. They make routing decisions but produce no data outputs.

* **IfElseNode** - Binary gate for true/false routing decisions
* **@ifelse** - Decorator to create an IfElseNode from a boolean function
* **RouteNode** - Routes execution to target nodes based on a function's return value
* **@route** - Decorator to create a RouteNode from a function
* **END** - Sentinel indicating execution should terminate
* **GateNode** - Abstract base class for all gate types

## @ifelse Decorator

Create an IfElseNode from a boolean function. Simplest way to branch on true/false.

### Signature

```python
def ifelse(
    when_true: str,
    when_false: str,
    *,
    cache: bool = False,
    hide: bool = False,
    default_open: bool = True,
    name: str | None = None,
    rename_inputs: dict[str, str] | None = None,
    emit: str | tuple[str, ...] | None = None,
    wait_for: str | tuple[str, ...] | None = None,
) -> Callable[[Callable[..., bool]], IfElseNode]: ...
```

### Parameters

| Parameter       | Type                   | Default  | Description                                                                                               |
| --------------- | ---------------------- | -------- | --------------------------------------------------------------------------------------------------------- |
| `when_true`     | `str`                  | required | Target when function returns `True` (pass `END` to terminate)                                             |
| `when_false`    | `str`                  | required | Target when function returns `False` (pass `END` to terminate)                                            |
| `cache`         | `bool`                 | `False`  | Whether to cache routing decisions                                                                        |
| `hide`          | `bool`                 | `False`  | Whether to hide from visualization                                                                        |
| `default_open`  | `bool`                 | `True`   | If True, targets may execute before the gate runs. If False, targets are blocked until the gate executes. |
| `name`          | `str \| None`          | `None`   | Node name (default: function name)                                                                        |
| `rename_inputs` | `dict \| None`         | `None`   | Mapping to rename inputs `{old: new}`                                                                     |
| `emit`          | `str \| tuple \| None` | `None`   | Ordering-only output(s). Auto-produced when the gate runs                                                 |
| `wait_for`      | `str \| tuple \| None` | `None`   | Ordering-only input(s). Gate waits until these values are fresh                                           |

### Return Value

The decorated function must return exactly `True` or `False` (not truthy/falsy values):

* `True` - Routes to `when_true` target
* `False` - Routes to `when_false` target

### Basic Usage

```python
from hypergraph import ifelse, END

@ifelse(when_true="process", when_false="skip")
def is_valid(data: dict) -> bool:
    return data.get("valid", False)
```

### With END

```python
@ifelse(when_true="continue", when_false=END)
def should_continue(count: int) -> bool:
    return count < 10  # False terminates execution
```

### Input Renaming

```python
@ifelse(when_true="yes", when_false="no", rename_inputs={"x": "input_value"})
def is_positive(x: int) -> bool:
    return x > 0
```

***

## IfElseNode Class

Binary gate that routes based on boolean decision. Use `@ifelse` decorator for most cases.

### Constructor

```python
def __init__(
    self,
    func: Callable[..., bool],
    when_true: str,
    when_false: str,
    *,
    cache: bool = False,
    hide: bool = False,
    default_open: bool = True,
    name: str | None = None,
    rename_inputs: dict[str, str] | None = None,
) -> None: ...
```

### Properties

#### `name: str`

Public node name.

#### `inputs: tuple[str, ...]`

Input parameter names from function signature.

#### `outputs: tuple[str, ...]`

Always empty tuple. Gates produce no data outputs.

#### `targets: list[str]`

Always `[when_true, when_false]` (2 elements).

```python
@ifelse(when_true="process", when_false=END)
def check(x: int) -> bool:
    return x > 0

print(check.targets)  # ['process', END]
```

#### `when_true: str`

Target when function returns `True`.

#### `when_false: str`

Target when function returns `False`.

#### `descriptions: dict[bool, str]`

Fixed `{True: "True", False: "False"}`.

#### `cache: bool`

Whether routing decisions are cached.

#### `func: Callable`

The wrapped boolean function.

#### `is_async: bool`

Always `False`. Routing functions must be synchronous.

#### `is_generator: bool`

Always `False`. Routing functions cannot be generators.

#### `definition_hash: str`

SHA256 hash of function source code.

### Methods

#### `has_default_for(param: str) -> bool`

Check if parameter has a default value.

#### `get_default_for(param: str) -> Any`

Get default value for a parameter. Raises `KeyError` if no default.

#### `with_name(name: str) -> IfElseNode`

Return a new node with a different name.

#### `with_inputs(mapping=None, /, **kwargs) -> IfElseNode`

Return a new node with renamed inputs.

#### `__call__(*args, **kwargs) -> bool`

Call the boolean function directly.

#### `__repr__() -> str`

Informative string representation.

```python
print(repr(check))
# IfElseNode(check, true=process, false=END)
```

***

## @route Decorator

Create a RouteNode from a routing function.

### Signature

```python
def route(
    targets: list[str] | dict[str, str],
    *,
    fallback: str | None = None,
    multi_target: bool = False,
    cache: bool = False,
    hide: bool = False,
    default_open: bool = True,
    name: str | None = None,
    rename_inputs: dict[str, str] | None = None,
    emit: str | tuple[str, ...] | None = None,
    wait_for: str | tuple[str, ...] | None = None,
) -> Callable[[Callable], RouteNode]: ...
```

### Parameters

| Parameter       | Type                   | Default  | Description                                                                                               |
| --------------- | ---------------------- | -------- | --------------------------------------------------------------------------------------------------------- |
| `targets`       | `list` or `dict`       | required | Valid target node names. Dict values are descriptions for documentation.                                  |
| `fallback`      | `str \| None`          | `None`   | Default target if function returns `None` (pass `END` to terminate)                                       |
| `multi_target`  | `bool`                 | `False`  | If `True`, function returns list of targets                                                               |
| `cache`         | `bool`                 | `False`  | Whether to cache routing decisions                                                                        |
| `hide`          | `bool`                 | `False`  | Whether to hide from visualization                                                                        |
| `default_open`  | `bool`                 | `True`   | If True, targets may execute before the gate runs. If False, targets are blocked until the gate executes. |
| `name`          | `str \| None`          | `None`   | Node name (default: function name)                                                                        |
| `rename_inputs` | `dict \| None`         | `None`   | Mapping to rename inputs `{old: new}`                                                                     |
| `emit`          | `str \| tuple \| None` | `None`   | Ordering-only output(s). Auto-produced when the gate runs                                                 |
| `wait_for`      | `str \| tuple \| None` | `None`   | Ordering-only input(s). Gate waits until these values are fresh                                           |

### Return Value

The decorated function should return:

* `str` - Target node name to activate
* `END` - Terminate execution along this path
* `None` - Use fallback (if set) or activate no targets
* `list[str]` - Multiple targets (only with `multi_target=True`; entries may be `END`)

### Basic Usage

```python
from hypergraph import route, END

@route(targets=["process", "skip", END])
def decide(x: int) -> str:
    if x == 0:
        return END
    return "process" if x > 0 else "skip"
```

### With Descriptions

```python
@route(targets={
    "fast_path": "Use cached response for simple queries",
    "full_rag": "Full retrieval for complex queries",
    END: "Terminate if query is invalid",
})
def route_query(query_type: str) -> str:
    ...
```

### With Fallback

```python
@route(targets=["premium", "standard"], fallback="standard")
def route_by_tier(user_tier: str | None) -> str | None:
    if user_tier == "premium":
        return "premium"
    return None  # Falls back to "standard"
```

### Multi-Target Mode

```python
@route(targets=["notify", "log", "alert"], multi_target=True)
def choose_actions(severity: str) -> list[str]:
    actions = ["log"]  # Always log
    if severity == "critical":
        actions.extend(["notify", "alert"])
    return actions
```

**Note**: When `multi_target=True`, target nodes must have unique output names.

***

## RouteNode Class

Concrete gate that routes to target nodes based on a routing function.

### Constructor

```python
def __init__(
    self,
    func: Callable[..., str | list[str] | None],
    targets: list[str] | dict[str, str],
    *,
    fallback: str | None = None,
    multi_target: bool = False,
    cache: bool = False,
    hide: bool = False,
    default_open: bool = True,
    name: str | None = None,
    rename_inputs: dict[str, str] | None = None,
) -> None: ...
```

Parameters are the same as `@route`. Use the decorator for most cases.

### Properties

#### `name: str`

Public node name.

```python
@route(targets=["a", "b"])
def decide(x: int) -> str:
    return "a"

print(decide.name)  # "decide"
```

#### `inputs: tuple[str, ...]`

Input parameter names from function signature.

```python
@route(targets=["a", "b"])
def decide(x: int, threshold: float = 0.5) -> str:
    return "a" if x > threshold else "b"

print(decide.inputs)  # ("x", "threshold")
```

#### `outputs: tuple[str, ...]`

Always empty tuple. Gates produce no data outputs.

```python
print(decide.outputs)  # ()
```

#### `targets: list[str]`

List of valid target names.

```python
@route(targets=["process", END])
def decide(x: int) -> str:
    return "process"

print(decide.targets)  # ['process', END]
```

#### `descriptions: dict[str, str]`

Target descriptions (empty if not provided).

```python
@route(targets={"a": "First option", "b": "Second option"})
def decide(x: int) -> str:
    return "a"

print(decide.descriptions)  # {"a": "First option", "b": "Second option"}
```

#### `fallback: str | None`

Default target when function returns `None`.

```python
@route(targets=["a", "b"], fallback="b")
def decide(x: int) -> str | None:
    return None

print(decide.fallback)  # "b"
```

#### `multi_target: bool`

Whether function returns list of targets.

```python
@route(targets=["a", "b"], multi_target=True)
def decide(x: int) -> list[str]:
    return ["a", "b"]

print(decide.multi_target)  # True
```

#### `cache: bool`

Whether routing decisions are cached.

```python
@route(targets=["a", "b"], cache=True)
def decide(x: int) -> str:
    return "a"

print(decide.cache)  # True
```

#### `func: Callable`

The wrapped routing function.

```python
@route(targets=["a", "b"])
def decide(x: int) -> str:
    return "a"

print(decide.func.__name__)  # "decide"
```

#### `is_async: bool`

Always `False`. Routing functions must be synchronous.

#### `is_generator: bool`

Always `False`. Routing functions cannot be generators.

#### `definition_hash: str`

SHA256 hash of function source code.

```python
print(len(decide.definition_hash))  # 64
```

### Methods

#### `has_default_for(param: str) -> bool`

Check if parameter has a default value.

```python
@route(targets=["a", "b"])
def decide(x: int, threshold: float = 0.5) -> str:
    return "a"

print(decide.has_default_for("x"))          # False
print(decide.has_default_for("threshold"))  # True
```

#### `get_default_for(param: str) -> Any`

Get default value for a parameter.

```python
print(decide.get_default_for("threshold"))  # 0.5
decide.get_default_for("x")  # Raises KeyError
```

#### `with_name(name: str) -> RouteNode`

Return a new node with a different name.

```python
renamed = decide.with_name("router")
print(renamed.name)  # "router"
print(decide.name)   # "decide" (unchanged)
```

#### `with_inputs(mapping=None, /, **kwargs) -> RouteNode`

Return a new node with renamed inputs.

```python
adapted = decide.with_inputs(x="value", threshold="cutoff")
print(adapted.inputs)  # ("value", "cutoff")
```

#### `__call__(*args, **kwargs)`

Call the routing function directly.

```python
result = decide(5)
print(result)  # "a"
```

#### `__repr__() -> str`

Informative string representation.

```python
print(repr(decide))
# RouteNode(decide, targets=['a', 'b'])
```

***

## END Sentinel

Singleton sentinel indicating execution should terminate along this path.

### Usage

```python
from hypergraph import END

@route(targets=["process", END])
def decide(x: int) -> str:
    if x == 0:
        return END  # Terminate
    return "process"
```

`END` is a singleton instance of a `str` subclass. This means:

* Routing functions can be annotated `-> str` and return `END` without typing friction.
* `END` has a clean string display (`repr(END) == "END"`, `str(END) == "END"`).
* Identity checks (`target is END`) uniquely distinguish the sentinel from any user-supplied string.
* `END` does not equal the literal string `"END"` (its underlying value is intentionally hidden to prevent collisions).

```python
print(END)              # END
print(repr(END))        # END
isinstance(END, str)    # True
END is END              # True
END == "END"            # False
```

### Checking for END

Always use identity (`is`), not equality:

```python
if target is END:
    print("Terminating")
```

### Collision Detection

If a routing function returns a plain string that happens to match `END`'s underlying value (for example, an LLM emitting the reserved token), the gate raises a `ValueError` instead of silently terminating. Use the `END` sentinel directly to terminate.

***

## GateNode Base Class

Abstract base class for all gate types. You typically won't use this directly.

### Attributes

All GateNode subclasses have:

```python
node.name: str                           # Public node name
node.inputs: tuple[str, ...]             # Input parameter names
node.outputs: tuple[str, ...]            # Always empty for gates
node.targets: list[str]      # Valid target names
node.descriptions: dict[str, str]  # Target descriptions
node.cache: bool                         # Whether to cache decisions
```

***

## Validation Errors

### At Decoration Time

**Async functions not allowed:**

```python
@route(targets=["a"])
async def decide(x: int) -> str:  # ERROR
    return "a"

# TypeError: Routing function 'decide' cannot be async.
# Routing decisions should be fast and based on already-computed values.
```

**Generator functions not allowed:**

```python
@route(targets=["a"])
def decide(x: int):  # ERROR
    yield "a"

# TypeError: Routing function 'decide' cannot be a generator.
# Routing functions must return a single decision, not yield multiple.
```

**Empty targets:**

```python
@route(targets=[])  # ERROR
def decide(x: int) -> str:
    return "a"

# ValueError: RouteNode 'decide' must have at least one target.
```

**Fallback with multi\_target:**

```python
@route(targets=["a"], fallback="a", multi_target=True)  # ERROR
def decide(x: int) -> list[str]:
    return ["a"]

# ValueError: RouteNode 'decide' cannot have both fallback and multi_target=True.
```

**IfElseNode with same targets:**

```python
@ifelse(when_true="same", when_false="same")  # ERROR
def check(x: int) -> bool:
    return x > 0

# ValueError: IfElseNode 'check' has the same target for both branches.
# when_true='same' == when_false='same'
```

**String 'END' as target:**

```python
@ifelse(when_true="END", when_false="process")  # ERROR
def check(x: int) -> bool:
    return x > 0

# ValueError: Gate 'check' has 'END' as a string target.
# Use 'from hypergraph import END' and use END directly.
```

### At Graph Build Time

**Invalid target (not a node in graph):**

```python
@route(targets=["nonexistent"])
def decide(x: int) -> str:
    return "nonexistent"

Graph([decide])
# GraphConfigError: Route target 'nonexistent' references unknown node.
```

**Self-targeting:**

```python
@route(targets=["decide"])
def decide(x: int) -> str:
    return "decide"

Graph([decide])
# GraphConfigError: RouteNode 'decide' cannot target itself.
```

**Multi-target with shared outputs:**

```python
@node(output_name="result")
def path_a(x: int) -> int:
    return x

@node(output_name="result")  # Same output name!
def path_b(x: int) -> int:
    return x

@route(targets=["path_a", "path_b"], multi_target=True)
def decide(x: int) -> list[str]:
    return ["path_a", "path_b"]

Graph([decide, path_a, path_b])
# GraphConfigError: Multiple nodes produce 'result': path_a, path_b
# With multi_target=True, target nodes must have unique output names.
```

### At Runtime

**Invalid return value:**

```python
@route(targets=["a", "b"])
def decide(x: int) -> str:
    return "c"  # Not in targets!

graph = Graph([decide, a, b])
result = runner.run(graph, {"x": 5})

# result.status == RunStatus.FAILED
# result.error: ValueError: invalid target 'c'. Valid targets: ['a', 'b']
```

**Wrong return type for multi\_target:**

```python
@route(targets=["a", "b"], multi_target=True)
def decide(x: int) -> str:  # Should return list!
    return "a"

result = runner.run(graph, {"x": 5})
# result.error: TypeError: multi_target=True but returned str, expected list
```

**IfElseNode returns non-bool:**

```python
@ifelse(when_true="yes", when_false="no")
def check(x: int) -> bool:
    return 1  # Truthy, but not True!

result = runner.run(graph, {"x": 5})
# result.error: TypeError: IfElseNode 'check' must return exactly True or False, got int
```

IfElseNode strictly validates boolean returns. `1`, `"yes"`, or any truthy value that isn't `True` will fail.

***

## Complete Example

````python
from hypergraph import Graph, node, route, END, SyncRunner

# Processing nodes
@node(output_name="analysis")
def analyze(document: str) -> dict:
    return {
        "length": len(document),
        "has_code": "```" in document,
        "language": detect_language(document),
    }

# Routing node
@route(targets={
    "code_processor": "Handle documents with code blocks",
    "text_processor": "Handle plain text documents",
    END: "Skip empty documents",
})
def route_document(analysis: dict) -> str:
    if analysis["length"] == 0:
        return END
    if analysis["has_code"]:
        return "code_processor"
    return "text_processor"

# Target nodes
@node(output_name="result")
def code_processor(document: str, analysis: dict) -> str:
    return f"Processed code document ({analysis['language']})"

@node(output_name="result")
def text_processor(document: str, analysis: dict) -> str:
    return f"Processed text document ({analysis['length']} chars)"

# Build and run
graph = Graph([analyze, route_document, code_processor, text_processor])
runner = SyncRunner()

# Route to code_processor
result = runner.run(graph, {"document": "```python\nprint('hello')\n```"})
print(result["result"])  # "Processed code document (python)"

# Route to text_processor
result = runner.run(graph, {"document": "Hello, world!"})
print(result["result"])  # "Processed text document (13 chars)"

# Route to END (no result)
result = runner.run(graph, {"document": ""})
print("result" in result.values)  # False
````


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://gilad-rubin.gitbook.io/hypergraph/api-reference/gates.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
