Skip to content

Writing Plugins

Plugin structure

A plugin is a TOML file with this structure:

[plugin]
name = "my-plugin"
plugin_version = "0.1.0"
vanya_version = "0.1"
language = "python"
file_extension = ".py"
[types.my_type]
allowed_children = []
modifiers = { key = "quoted" }
template = "ctx = setup()"
[[types.my_type.patterns]]
name = "do-something"
pattern = "do something with <value:quoted>"
template = "do_something({{ value }})"

Loading custom plugins

There are two ways to load custom plugins:

Option 1: —plugin-dir flag

Point vanya to a directory containing plugin TOML files:

Terminal window
vanya check --plugin-dir ./plugins mytest.vanya
vanya build --plugin-dir ./plugins mytest.vanya -o out/

All .toml files in the directory (except vanya.config.toml) will be loaded as plugins.

Option 2: vanya.config.toml

Create a vanya.config.toml file in your project:

[project]
plugins = [
"plugins/my-plugin.toml",
"plugins/another-plugin.toml",
]

Then reference it with --config:

Terminal window
vanya check --config vanya.config.toml mytest.vanya
vanya build --config vanya.config.toml mytest.vanya -o out/

Plugin paths in the config file are relative to the config file’s location.

Using your plugin

Once loaded, reference your plugin in .vanya files:

Vanya test scenario (vanya-lang.org/v0.1)
Script title: "Test with custom plugin"
Within the MyContext (plugin: my-plugin, type: my_type)
do something with "value"

Sections

[plugin]

Plugin metadata:

[plugin]
name = "my-plugin" # Required: unique plugin identifier
plugin_version = "0.1.0" # Optional: plugin version (default: "0.1.0")
vanya_version = "0.1" # Optional: vanya compatibility version (default: "0.1")
language = "python" # Optional: target language (default: "python")
file_extension = ".py" # Optional: output file extension (default: ".py")

[types.*]

Define available types within the plugin:

[types.browser]
allowed_children = ["element", "failure_block"]
modifiers = { url = "quoted", headless = "quoted" }
template = """
ctx = playwright.chromium.launch().new_page()
{% if url %}ctx.goto("{{ url }}"){% endif %}
"""
[[types.browser.patterns]]
name = "click"
pattern = "click <target:unquoted>"
modifiers = { timeout = "quoted" }
template = 'ctx.locator("{{ target }}").click()'

Type fields:

  • allowed_children - List of type names that can be nested within this type
  • modifiers - Map of modifier names to their value types ("quoted" or "unquoted")
  • template - Jinja2 template for the type’s initialization code
  • patterns - Array of action patterns (use [[types.*.patterns]] syntax) or the string "passthrough"

[[types.*.patterns]]

Define action patterns using TOML array-of-tables syntax:

[[types.browser.patterns]]
name = "go-to"
pattern = "go to <url:quoted>"
modifiers = { timeout = "quoted" }
template = 'ctx.goto("{{ url }}")'
[[types.browser.patterns]]
name = "click"
pattern = "click <target:unquoted>"
template = 'ctx.locator("{{ target }}").click()'
[[types.browser.patterns]]
name = "expect-failure"
pattern = "expect failure"
modifiers = { error = "quoted", capture = "quoted" }
returns = "failure_block"
template = '''
{% if error %}
with pytest.raises({{ error }}):
{{ children | indent(4) }}
{% else %}
with pytest.raises(Exception):
{{ children | indent(4) }}
{% endif %}
'''

Pattern fields:

  • name - Unique identifier for the pattern within the type
  • pattern - The pattern string with slot placeholders
  • modifiers - Optional map of modifier names to value types
  • template - Jinja2 template for the generated code
  • returns - Optional type name for actions that create nested blocks

Passthrough types

For types that pass through child actions without modification:

[types.failure_block]
patterns = "passthrough"

Slot types

Slots capture values from action statements using <name:type> syntax:

  • quoted - String literal (with quotes), e.g., "hello"
  • unquoted - Identifier without quotes, e.g., button
  • a|b|c - Union of keywords, matches one of the listed values

Examples:

pattern = "go to <url:quoted>" # Matches: go to "http://example.com"
pattern = "click <target:unquoted>" # Matches: click submit_button
pattern = "see <q:a|an|any|no> <el:unquoted>" # Matches: see any dialog

Template variables

Templates use Jinja2 syntax with the following variables:

  • {{ slot_name }} - Captured slot value (e.g., {{ url }}, {{ target }})
  • {{ children }} - Rendered child blocks (for actions with returns)
  • {{ ctx }} - Current context variable name (snake_case of the block name)
  • {{ name }} - The within block’s display name
  • Modifier values are available by their key names (e.g., {{ timeout }})

Template filters

The following Jinja2 filters are available:

  • indent(n) - Indent text by n spaces
  • snake_case - Convert text to snake_case

Example:

template = '''
with pytest.raises({{ error }}):
{{ children | indent(4) }}
'''

Complete example

Here is a minimal API testing plugin:

[plugin]
name = "python-requests"
plugin_version = "0.1.0"
vanya_version = "0.1"
language = "python"
file_extension = ".py"
[types.api]
allowed_children = ["failure_block"]
modifiers = { base = "quoted" }
template = """
ctx = requests.Session()
ctx.base_url = "{{ base }}"
"""
[[types.api.patterns]]
name = "get"
pattern = "GET <path:quoted>"
modifiers = { headers = "quoted", timeout = "quoted" }
returns = "http_response"
template = 'http_response = ctx.get("{{ path }}")'
[[types.api.patterns]]
name = "post"
pattern = "POST <path:quoted> body <body:quoted>"
returns = "http_response"
template = 'http_response = ctx.post("{{ path }}", json={{ body }})'
[types.http_response]
[[types.http_response.patterns]]
name = "expect-status"
pattern = "expect status <code:unquoted>"
template = "assert http_response.status_code == {{ code }}"
[types.failure_block]
patterns = "passthrough"