> For the complete documentation index, see [llms.txt](https://docs.augelab.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.augelab.com/key-features/headless/scripting-reference.md).

# Scripting Reference

This page documents the **public headless scripting API** exposed by the `studio` Python package.

It focuses on these public symbols:

* `StudioScenario`
* `install_logging_event`
* `enable_logging_stdout`
* `disable_logging_stdout`

If you haven't run a scenario headlessly before, read the Headless section first and then come back here for API details.

***

## Imports

All of the APIs documented here are exported from the top-level `studio` package:

```python
from studio import (
	StudioScenario,
	install_logging_event,
	enable_logging_stdout,
	disable_logging_stdout,
)
```

***

## Type Stubs

The public headless surface can be found here:

```python
from __future__ import annotations

from typing import Any, Callable, Optional


def enable_logging_stdout() -> None: ...
def disable_logging_stdout() -> None: ...
def install_logging_event(event: Callable[[str], None]) -> None: ...


class StudioScenario:
	def __init__(self, *, verification_code: str = "") -> None: ...

	def load_scenario(self, path: str) -> Optional[StudioScenario]: ...
	def load_scenario_raw(self, content: str) -> bool: ...
	def save_scenario(self, path: str) -> bool: ...
	def clear_scenario(self) -> bool: ...
	def disable_load_errors(self) -> StudioScenario: ...
	def load_custom_nodes(self, path: str) -> bool: ...

	def get_ports(
		self,
		input_op_title: str = "Subsystem In",
		output_op_title: str = "Subsystem Out",
	) -> dict[str, list[str]]: ...
	def get_name(self) -> str: ...

	def run(self, args: tuple[Any, ...] = ()) -> tuple[list[Any], ...]: ...
	def run_server(
		self,
		inputs: tuple[Any, ...] = (),
		host: str = "127.0.0.1",
		port: int = 8080,
		display_controls: bool = False,
		header: str = "Scenario Server",
	) -> None: ...
	def cleanup(self) -> bool: ...

	def enable_logging_stdout(self) -> StudioScenario: ...
	def disable_logging_stdout(self) -> StudioScenario: ...
	def install_logging_hook(self, hook: Callable[[str, int], None], level: int = 0) -> StudioScenario: ...

	def installStartEvent(self, event: Callable[..., None]) -> StudioScenario: ...
	def installEndEvent(self, event: Callable[..., None]) -> StudioScenario: ...
	def installStepStartEvent(self, event: Callable[..., None]) -> StudioScenario: ...
	def installStepEndEvent(self, event: Callable[..., None]) -> StudioScenario: ...
```

***

## `StudioScenario`

`StudioScenario` is the main entry point for **loading and running** a `.pmod` file without the desktop UI.

### Constructor

```python
scenario = StudioScenario(verification_code="...")
```

#### `verification_code`

This value is used to configure licensing for headless execution.

Common patterns:

* **Local machine / server:** pass the verification code string.
* **Docker / CI:** mount a license file into the container and pass its **path** as `verification_code`.

For non-interactive environments (Docker/CI/services), always pass `verification_code` explicitly. { % endhint %}

***

### Typical lifecycle

In most scripts you will:

1. Create the scenario (`StudioScenario(...)`)
2. Load a `.pmod` (`load_scenario(...)`)
3. Run it (`run(...)`)
4. Cleanup (`cleanup()`)

```python
import os

from studio import StudioScenario, enable_logging_stdout


def main() -> None:
	# Optional: prints runtime logs to stdout (useful when debugging).
	enable_logging_stdout()

	verification_code = os.environ.get("AUGELAB_VERIFICATION_CODE", "")
	scenario = StudioScenario(verification_code=verification_code)

	try:
		loaded = scenario.load_scenario("your_scenario.pmod")
		if loaded is None:
			raise FileNotFoundError("Could not load .pmod file")

		# If your scenario has N input ports, you must pass exactly N args.
		result = scenario.run(args=())
		print("Scenario result:", result)
	finally:
		scenario.cleanup()


if __name__ == "__main__":
	main()
```

`cleanup()` is important in long-running processes (services, batch jobs) to release memory and reset runtime state. { % endhint %}

***

### Loading a `.pmod`

```python
scenario.load_scenario("path/to/scenario.pmod")
```

* Returns `StudioScenario` on success.
* Returns `None` if the path does not exist.

If your scenario references external resources, keep the same directory structure used on desktop (see the “Transferring .pmod files” section).

***

### Running a scenario

```python
outputs = scenario.run(args=(... ,))
```

The runtime validates that the number of arguments matches the number of scenario inputs.

If your `.pmod` has:

* 0 inputs: call `scenario.run()` or `scenario.run(())`
* 1 input: call `scenario.run((value,))`
* 2 inputs: call `scenario.run((value1, value2))`

`args` must be a tuple. For a single input, remember the trailing comma: `(value,)`. { % endhint %}

***

## More `StudioScenario` Methods

This section covers additional public methods you can use for automation and debugging.

### Loading without failing on missing resources

If you are moving scenarios between machines/containers and some resources may be unavailable, you can choose to load more permissively:

```python
from studio import StudioScenario

scenario = StudioScenario(verification_code="YOUR_CODE")
scenario.disable_load_errors()
scenario.load_scenario("scenario.pmod")
```

### Load / save

```python
scenario.load_scenario_raw(content="...")
scenario.save_scenario("out.pmod")
scenario.clear_scenario()
```

### Custom nodes

Load custom node Python modules from a folder:

```python
scenario.load_custom_nodes("path/to/custom_nodes")
```

This dynamically imports Python files. Only load trusted code. { % endhint %}

### Scenario name

```python
name = scenario.get_name()
```

### Ports (scenario inputs/outputs)

You can query the input/output port names:

```python
ports = scenario.get_ports()
print("inputs:", ports["inputs"])
print("outputs:", ports["outputs"])
```

### Run a web server (optional)

If you want to expose a scenario runner over HTTP:

```python
scenario.run_server(host="0.0.0.0", port=8080)
```

### Events

Install lifecycle callbacks around scenario execution:

```python
def on_start() -> None:
	print("Scenario started")


scenario.installStartEvent(on_start)
```

***

## Logging Helpers

AugeLab Studio has a runtime logger used by blocks and scenarios. In headless mode you typically want one of these:

* print logs to stdout while developing
* forward logs into your own logging system

These helpers are **global** (they affect the runtime logger used by your headless scenario).

***

### `enable_logging_stdout()`

Enables “friendly” runtime logging to stdout.

Use this when you want to see block/scenario logs in the console:

```python
from studio import enable_logging_stdout

enable_logging_stdout()
```

***

### `disable_logging_stdout()`

Disables runtime logging to stdout.

Useful when:

* you are using `install_logging_event(...)` and want to avoid duplicated logs
* you want clean output (only your own `print(...)` / logger output)

```python
from studio import disable_logging_stdout

disable_logging_stdout()
```

***

### `install_logging_event(event: Callable[[str], None])`

Installs a callback that receives runtime log messages.

This is the easiest way to integrate AugeLab runtime logs into your own logging framework.

```python
import logging

from studio import install_logging_event, disable_logging_stdout

logger = logging.getLogger("augelab")


def forward_to_python_logging(msg: str) -> None:
	logger.info(msg)


disable_logging_stdout()
install_logging_event(forward_to_python_logging)
```

### `StudioScenario.install_logging_hook(hook, level=0)`

If you prefer per-scenario hook installation (with filtering by level), use:

```python
def hook(msg: str, level: int) -> None:
	print(level, msg)


scenario.install_logging_hook(hook, level=20)
```

Keep your callback fast. If you need to do heavy work (I/O, network), consider buffering messages and processing them in another thread/process. { % endhint %}

***

## Recommended Patterns

### 1) Environment variable for licensing

For scripts that run in different environments (local vs CI vs Docker), passing `verification_code` via an environment variable keeps code portable:

```python
import os
from studio import StudioScenario

scenario = StudioScenario(verification_code=os.environ["AUGELAB_VERIFICATION_CODE"])
```

### 2) Always cleanup with `try/finally`

```python
from studio import StudioScenario

scenario = StudioScenario(verification_code="YOUR_CODE")
try:
	scenario.load_scenario("scenario.pmod")
	scenario.run(())
finally:
	scenario.cleanup()
```
