# 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()
```


---

# 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://docs.augelab.com/key-features/headless/scripting-reference.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.
