---
updatedAt: 2026-04-14T09:46:02.000Z
agentTools:
  projectIndex: https://developers.newo.ai/llms.txt
---

# Skill scripts and the Newo Script Language

Every skill in the Newo Agent Framework executes a script written in one of two templating languages: the Newo Script Language (NSL), a Jinja2-based language stored in `.nsl` files, or the legacy Guidance format, a Handlebars-like language stored in `.nslg` files. This article is a comprehensive reference for both syntaxes, covering control structures, role tags, LLM generation, action calling, and real-world patterns.

## NSL overview

NSL (Newo Script Language) is the primary scripting language for Newo agent skills. It is built on Jinja2 templating and extends it with platform-specific actions, role tags for LLM prompting, and access to Python standard library modules like `json`, `re`, and `datetime`.

NSL scripts are stored in `.nsl` files and executed by the `nsl` runner type. The language serves two purposes simultaneously:

* **Logic execution** --- Variable assignment, conditionals, loops, action calls, and data manipulation
* **Prompt construction** --- Building system/user/assistant messages that are sent to an LLM via `Gen()` or `GenStream()`

## Syntax reference

NSL uses Jinja2's delimiter system. The table below summarizes the four delimiter types and their purposes.

| **Delimiter**       | **Purpose**                          | **Example**                                                             |
| ------------------- | ------------------------------------ | ----------------------------------------------------------------------- |
| `{% ... %}`         | Control blocks (set, if, for, macro) | `{% set user_id = GetUser().id \| string %}`                            |
| `{{ ... }}`         | Expressions and action calls         | `{{SendSystemEvent(eventIdn="extend_session", connectorIdn="system")}}` |
| `{# ... #}`         | Comments (stripped from output)      | `{# Define user's phone number in background #}`                        |
| `{{tag}}...{{end}}` | Role tags for LLM message sections   | `{{system}}You are a helpful assistant.{{end}}`                         |

### Variable assignment with `set`

Variables are assigned using `{% set %}` blocks. The value can be a literal, a function call, a filter chain, or a complex expression:

```
{% set user_id = GetUser().id | string %}
{% set current_datetime = GetDateTime(format="datetime", timezone=project_business_time_zone, weekday=True) %}
{% set schema = {
  "type": "object",
  "properties": {
    "revenue_model_key": {
      "type": "string",
      "enum": revenueModels | list
    }
  },
  "required": ["revenue_model_key"]
} %}
```

Variables can hold strings, numbers, booleans, lists, dictionaries, and objects returned by platform functions.

### Conditionals with `if`/`elif`/`else`

NSL supports full Jinja2 conditional blocks:

```
{% if GetTriggeredAct().arguments.isSharedPhoneNumber == "true" %}
    {{SetPersonaAttribute(id=user_id, field="temporal", value="True")}}
{% else %}
    {% set detected_phone_number_with_country_code = GetActor().externalId | string %}
    {{SetPersonaAttribute(id=user_id, field="phone_number", value=detected_phone_number_with_country_code)}}
{% endif %}
```

Conditions can use standard comparison operators (`==`, `!=`, `>`, `<`), the `not` keyword, the `in` operator for membership testing, and logical operators `and` and `or`:

```
{% if not transcript.strip() or (not GetUser().name ~ ":" in transcript) %}
    {{Return()}}
{% endif %}
```

### Loops with `for`

`{% for %}` blocks iterate over sequences. Inside a loop, the loop body can modify outer-scope variables (see **Variable scoping** below):

```
{% set valid_bookings = [] %}

{% for booking in bookings %}
    {% set booking_date_time = booking.datetime %}
    {% if datetime.datetime.strptime(booking_date_time, "%Y-%m-%d %H:%M").date() >= datetime.datetime.strptime(current_datetime, "%Y-%m-%d").date() %}
        {{ valid_bookings.append(booking) }}
    {% endif %}
{% endfor %}
```

### Comments

Comments are enclosed in `{# ... #}` and are completely stripped from the script output. They do not reach the LLM or appear in any rendered text:

```
{# TODO: Define whether detected_phone_number must be reset on conversation_ended or not #}
{# Perform semaphore analysis #}
```

### Multi-line set blocks

For assigning long multi-line strings (such as prompt instructions), NSL supports the `{% set variable %}...{% endset %}` block form:

```
{%- set follow_up_instruction %}
This is a **follow-up check-in** turn. Your sole goal is to verify the user is still connected.

**DO**
- Keep the user engaged with a brief check-in question.

**DON'T**
- Don't proceed with operational steps.
{%- endset %}
```

## Variable scoping

NSL has a distinctive scoping behavior that differs from standard Jinja2: **variables set inside inner blocks (such as `if` or `for`) are visible in the outer scope after the block ends**. This means a variable assigned inside a conditional branch or a loop iteration remains accessible after the block closes.

This behavior is used throughout the codebase. For example, the pattern of building a list inside a `for` loop by calling `.append()` on an outer-scope list works because the list reference is shared:

```
{% set email_actors = [] %}
{% for actor in system_actors %}
    {% if actor.externalId.startswith("email_actor_") %}
        {{email_actors.append(actor)}}
    {% endif %}
{% endfor %}
{# email_actors is populated here with all matching actors #}
```

Similarly, variables set inside `if` blocks are available afterwards:

```
{% if base_instruction.strip() %}
    {% set fast_prompt = prompt_fast_compile(user_id=user_id, order="before_gen", conversation_channel=conversation_channel, base_instruction=base_instruction) %}
{% else %}
    {% set fast_prompt = GetState(name="fast_prompt") %}
{% endif %}
{# fast_prompt is accessible here regardless of which branch ran #}
```

:::
**⚠️ CAUTION**

Because inner-scope variables leak into the outer scope, be careful with variable naming inside loops and conditionals. Reusing a variable name in a nested block will overwrite the outer value.
:::

## Filters

NSL supports standard Jinja2 filters via the pipe (`|`) operator. Filters transform a value and can be chained:

```
{% set user_id = GetUser().id | string %}
{% set actors = actors | map(attribute='id') | map('string') | list %}
{% set revenue_models = revenueModels | list %}
```

Common filters used in NSL scripts:

| **Filter**           | **Description**                                    | **Example**                                |
| -------------------- | -------------------------------------------------- | ------------------------------------------ |
| `string`             | Converts a value to its string representation      | `GetUser().id \| string`                   |
| `int`                | Converts a string to an integer                    | `GetState(name="followup_counter") \| int` |
| `list`               | Converts an iterable to a list                     | `revenueModels \| list`                    |
| `map(attribute=...)` | Extracts an attribute from each item in a sequence | `actors \| map(attribute='id')`            |
| `map('filter')`      | Applies a filter to each item                      | `actors \| map('string')`                  |
| `length`             | Returns the length of a sequence or string         | `actors \| length`                         |

NSL also provides access to Python string methods directly on string values (e.g., `.strip()`, `.startswith()`, `.replace()`, `.lower()`). These are not Jinja2 filters but are available because NSL exposes Python string objects:

```
{% set cleaned_prompt = re.sub("<\\|\\|.*?\\|\\|>", "", fast_prompt.strip()) %}
{% set conversation_meta = GetPersonaAttribute(id=user_id, field="conversation_meta").strip() %}
{% set sms_value = (sms_subscribed | string).lower() %}
```

## Role tags for LLM prompting

When a skill needs to call an LLM, the script constructs a prompt using **role tags**. These tags define the message sections (system, user, assistant) that are sent to the LLM as part of the chat completion request.

### NSL role tags

NSL uses a double-brace tag syntax without the Jinja2 block delimiters:

| **Tag**         | **Closes with** | **Purpose**                        |
| --------------- | --------------- | ---------------------------------- |
| `{{system}}`    | `{{end}}`       | Opens a system message section     |
| `{{user}}`      | `{{end}}`       | Opens a user message section       |
| `{{assistant}}` | `{{end}}`       | Opens an assistant message section |

A typical LLM-calling skill places the system prompt between `{{system}}` and `{{end}}`, then opens an `{{assistant}}` block where `Gen()` or `GenStream()` produces the response:

```
{{system}}
Translate a text specified in <TextToTranslate> to {{target_language}} language.
Return only the translated text without any explanation or additional text.

<TextToTranslate>
{{input_text}}
</TextToTranslate>
{{end}}

{{assistant}}
  {% set translated_text = Gen(temperature=0.1, topP=0, maxTokens=4000, skipFilter=True, thinkingBudget=420) %}
{{end}}

{{Return(val=translated_text)}}
```

:::
**🗒️ NOTE**

Text placed between a role tag and its closing `{{end}}` becomes the content of that message role in the LLM request. Jinja2 expressions and control blocks inside role-tagged sections are evaluated before the prompt is sent, so variables and function calls resolve to their values.
:::

### Inline role tag pattern

Many skills condense the role tags onto a single line for compact structured generation:

```
{{system}}{{prompt.strip()}}{{end}}{{assistant}}{% set result_json = Gen(jsonSchema=schema, validateSchema="True", temperature=0.2, topP=0, maxTokens=4000, skipFilter=True, thinkingBudget=425) %}{{end}}

{{Return(val=result_json)}}
```

### Multi-section prompts

More complex skills build multi-section prompts with extensive system instructions:

```
{{system}}
You are the call center supervisor agent. You are analyzing a session.

<Scenarios>
{{scenarios}}
</Scenarios>

<ConversationHistory>
{{transcript}}
</ConversationHistory>
{{end}}
{{assistant}}{% set result_json = Gen(jsonSchema=schema, validateSchema="True", temperature=0.2, topP=0, maxTokens=4000, skipFilter=True, thinkingBudget=1020) %}{{end}}

{{Return(val=result_json)}}
```

## Action calling from NSL scripts

NSL scripts interact with the Newo platform through **actions** --- built-in functions that read or write platform state, communicate with integrations, trigger events, and control execution flow. Actions are called using Jinja2 expression syntax (`{{ }}`).

### Platform data actions

These actions read from and write to the platform's data stores:

| **Action**                                          | **Description**                                              | **Example**                                                               |
| --------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------- |
| `GetUser()`                                         | Returns the current user object                              | `{% set user_id = GetUser().id \| string %}`                              |
| `GetActor()`                                        | Returns the current actor object                             | `GetActor().externalId`                                                   |
| `GetActors(...)`                                    | Queries actors by persona, integration, or connector         | `GetActors(personaId=user_id, integrationIdn="vapi")`                     |
| `GetTriggeredAct()`                                 | Returns the act (event payload) that triggered this skill    | `GetTriggeredAct().arguments.sectionName`                                 |
| `GetState(name=...)`                                | Reads a flow state field value                               | `GetState(name="user_reply_buffer")`                                      |
| `SetState(name=..., value=...)`                     | Writes a flow state field value                              | `{{SetState(name="waiting_mode_active", value="False")}}`                 |
| `GetPersonaAttribute(id=..., field=...)`            | Reads a persona attribute                                    | `GetPersonaAttribute(id=user_id, field="email")`                          |
| `SetPersonaAttribute(id=..., field=..., value=...)` | Writes a persona attribute                                   | `{{SetPersonaAttribute(id=user_id, field="full_name", value=user_name)}}` |
| `GetCustomerAttribute(field=...)`                   | Reads a customer (project-level) attribute                   | `GetCustomerAttribute(field="project_business_time_zone")`                |
| `GetSessionInfo()`                                  | Returns current session metadata                             | `GetSessionInfo().id \| string`                                           |
| `GetDateTime(...)`                                  | Returns the current date/time in a given format and timezone | `GetDateTime(format="datetime", timezone=tz, weekday=True)`               |
| `GetMemory(...)`                                    | Retrieves conversation memory                                | `GetMemory(count=10, maxLen=5000, filterByActorIds=actors)`               |

### Event and command actions

These actions communicate with other agents and integrations:

| **Action**                                                               | **Description**                                        |
| ------------------------------------------------------------------------ | ------------------------------------------------------ |
| `SendSystemEvent(eventIdn=..., connectorIdn=..., ...)`                   | Fires a system event with optional key-value arguments |
| `SendCommand(commandIdn=..., integrationIdn=..., connectorIdn=..., ...)` | Sends a command to an integration connector            |
| `UpdateUser(field=..., value=...)`                                       | Updates a field on the current user object             |

### Control flow actions

| **Action**        | **Description**                                           |
| ----------------- | --------------------------------------------------------- |
| `Return()`        | Exits the current skill immediately with no return value  |
| `Return(val=...)` | Exits the current skill and returns a value to the caller |

### LLM generation actions

| **Action**       | **Description**                                                           |
| ---------------- | ------------------------------------------------------------------------- |
| `Gen(...)`       | Synchronous LLM generation; returns the complete response as a string     |
| `GenStream(...)` | Streaming LLM generation; streams tokens to specified actors in real time |

### Calling other skills

Skills can call other skills defined in the same flow as if they were functions. The called skill's `idn` becomes the function name, and parameters are passed as keyword arguments:

```
{% set memory = get_memory(user_id=user_id, count="10", include_system="True") %}
{% set section = build_custom_section(sectionName=section_name, sectionValue=section_value) %}
{{log_event(message="New conversation started.")}}
```

The call is synchronous: the calling skill pauses until the sub-skill completes and returns a value.

## Python standard library access

NSL scripts have access to several Python standard library modules, which are used directly in expressions:

| **Module** | **Common usage**                                                   |
| ---------- | ------------------------------------------------------------------ |
| `json`     | `json.loads(...)`, `json.dumps(..., ensure_ascii=False, indent=2)` |
| `re`       | `re.sub(pattern, replacement, string, flags=re.DOTALL)`            |
| `datetime` | `datetime.datetime.strptime(date_string, format)`                  |

Example using `json` and `re` together:

```
{% set pre_analysis = json.loads(arguments.preAnalysis) %}
{% set cleaned = re.sub('<' ~ section_name ~ '>.*?</' ~ section_name ~ '>\\s*---\\s*', '', current_custom_prompt_section, flags=re.DOTALL) %}
{{Return(val=json.dumps({"date_added": current_datetime, "content": summary}, ensure_ascii=False, indent=2))}}
```

## Real code examples

### Example 1: Structured generation skill

This reusable skill accepts a prompt and a JSON schema, sends them to the LLM, and returns the structured result:

```
{{system}}{{prompt.strip()}}{{end}}{{assistant}}{% set result_json = Gen(
    jsonSchema=schema,
    validateSchema="True",
    temperature=0.2,
    topP=0,
    maxTokens=4000,
    skipFilter=True,
    thinkingBudget=425
) %}{{end}}

{{Return(val=result_json)}}
```

### Example 2: Streaming reply generation

This skill streams a conversational reply directly to a user-facing actor:

```
{{system}}{{prompt.strip()}}{{end}}{{assistant}}{% set agent_answer = GenStream(
    interruptMode="interruptWindow",
    interruptWindow=0.7,
    temperature=0.2,
    topP=0.5,
    maxTokens=4000,
    skipFilter=True,
    sendTo="actors",
    actorIds=[actor_id],
    thinkingBudget=thinking_budget
) %}{{end}}

{{Return(val=agent_answer)}}
```

### Example 3: Data processing with conditionals and loops

This skill filters a list of bookings to find only future, valid ones:

```
{% set cancellation_type = GetCustomerAttribute(field="project_attributes_setting_booking_cancellation_type") %}
{% set bookings = json.loads(bookings) %}
{% set valid_bookings = [] %}
{% set current_datetime = GetDateTime(timezone=GetCustomerAttribute(field="project_business_time_zone"), format="date") %}

{% for booking in bookings %}
    {% set booking_date_time = booking.datetime %}
    {% if datetime.datetime.strptime(booking_date_time, "%Y-%m-%d %H:%M").date() >= datetime.datetime.strptime(current_datetime, "%Y-%m-%d").date() %}
        {% if cancellation_type == "default" %}
            {% if booking.meta.manage_booking_url %}
                {{ valid_bookings.append(booking) }}
            {% endif %}
        {% else %}
            {{ valid_bookings.append(booking) }}
        {% endif %}
    {% endif %}
{% endfor %}

{{ Return(val=json.dumps(valid_bookings, ensure_ascii=False, indent=2)) }}
```

### Example 4: Conversation initialization skill

This skill sets up state when a new conversation begins, demonstrating event firing, conditional branching, and sub-skill calls:

```
{{SendSystemEvent(eventIdn="build_operating_phrase", connectorIdn="system")}}
{{SendSystemEvent(eventIdn="prepare_injecting_data", connectorIdn="system", global="True")}}
{{SetState(name="user_reply_buffer", value=" ")}}
{{SetState(name="corrected_memory", value=" ")}}
{% set user_id = GetUser().id | string %}

{% if is_feature_active(featureFlagName="multi_location") %}
    {{SetPersonaAttribute(id=user_id, field="multi_location_current_location", value="default")}}
{% endif %}

{{update_conversation_meta(userId=user_id)}}

{% if GetTriggeredAct().arguments.isSharedPhoneNumber == "true" %}
    {{SetPersonaAttribute(id=user_id, field="temporal", value="True")}}
{% else %}
    {% set detected_phone_number_with_country_code = GetActor().externalId | string %}
    {{SetPersonaAttribute(id=user_id, field="phone_number", value=detected_phone_number_with_country_code)}}

    {# Define user's phone number in background #}
    {{SendSystemEvent(
        eventIdn="define_user_phone_number",
        connectorIdn="system",
        user_id=user_id,
        user_phone_number_with_country_code=detected_phone_number_with_country_code
    )}}
{% endif %}

{% set project_business_time_zone = GetCustomerAttribute(field="project_business_time_zone") %}
{% set current_datetime = GetDateTime(format="datetime", timezone=project_business_time_zone, weekday=True) %}

{{initialize_session(user_id=user_id)}}
{{log_event(message="New conversation started.")}}
{{SendSystemEvent(eventIdn="extend_session", connectorIdn="system")}}
```

### Example 5: Boolean logic skill

This small skill evaluates business logic and returns a simple result:

```
{% set is_new_customer = not session.existing_customer %}
{% set is_not_dental = industry != "dental_industry" %}
{% set is_valuable = is_new_customer or is_not_dental %}
{% set is_lead = is_valuable and (session.is_lead or ("[L]" in intent_name)) %}

{% if is_lead %}
    {{Return(val="true")}}
{% else %}
    {{Return(val="false")}}
{% endif %}
```

## Legacy Guidance V1 format

The Guidance format is the original scripting language for Newo skills. It uses Handlebars-like syntax and is stored in `.nslg` files. While NSL is the preferred language for new skills, many existing skills still use Guidance, and both formats coexist within the same agent.

### Guidance syntax reference

| **Syntax**                               | **Purpose**                     | **NSL equivalent**                              |
| ---------------------------------------- | ------------------------------- | ----------------------------------------------- |
| `{{Set(name="x", value=...)}}`           | Variable assignment             | `{% set x = ... %}`                             |
| `{{#if condition}}...{{/if}}`            | Conditional block               | `{% if condition %}...{% endif %}`              |
| `{{#if condition}}...{{else}}...{{/if}}` | If/else                         | `{% if condition %}...{% else %}...{% endif %}` |
| `{{#system~}}...{{~/system}}`            | System prompt section           | `{{system}}...{{end}}`                          |
| `{{#assistant~}}...{{~/assistant}}`      | Assistant response section      | `{{assistant}}...{{end}}`                       |
| `{{!-- comment --}}`                     | Block comment                   | `{# comment #}`                                 |
| `{{! comment }}`                         | Inline comment                  | `{# comment #}`                                 |
| `Concat(a, b, c)`                        | String concatenation            | `a ~ b ~ c` (tilde operator)                    |
| `IsEmpty(text=...)`                      | Check if a string is empty      | `not value.strip()`                             |
| `Stringify(...)`                         | Convert to string               | `value \| string`                               |
| `GetValueJSON(obj=..., key=...)`         | Extract a key from JSON         | `json.loads(obj)[key]`                          |
| `StartNotInterruptibleBlock()`           | Begin non-interruptible section | (same in NSL)                                   |
| `StopNotInterruptibleBlock()`            | End non-interruptible section   | (same in NSL)                                   |

### Guidance role tags

Guidance uses a different syntax for role tags, with block-style delimiters and tilde (`~`) whitespace control:

```
{{#system~}}
You are a helpful assistant. Summarize the following conversation.

<Conversation>
{{transcript}}
</Conversation>

Summary:
{{~/system}}
{{#assistant~}}
{{Return(
  val=Gen(temperature=0.1, topP=0, skipFilter=True, thinkingBudget=1020)
)}}
{{~/assistant}}
```

The tilde (`~`) adjacent to the opening or closing tag strips whitespace on that side, keeping the prompt compact.

### Guidance code example: conversation started skill

This Guidance skill handles conversation initialization, showing `Set()`, `#if`, and event dispatch:

```
{{StartNotInterruptibleBlock()}}

{{Set(name="user_id", value=GetUser(field="id"))}}
{{Set(name="current_actor_integration_idn", value=GetActor(field="integrationIdn"))}}

{{!-- TODO: Refactor, split and speed up Conversation Started Skill --}}
{{#if current_actor_integration_idn == "vapi"}}
    {{SetCustomerAttribute(field="project_attributes_setting_voice_integration_service", value="VAPI Integration")}}
{{/if}}

{{SetState(name="user_reply_buffer", value=" ")}}
{{SetState(name="corrected_memory", value=" ")}}

{{#if is_feature_active(featureFlagName="multi_location")}}
    {{SetPersonaAttribute(id=user_id, field="multi_location_current_location", value="default")}}
{{/if}}

{{SendSystemEvent(eventIdn="build_operating_phrase", connectorIdn="system")}}
{{SendSystemEvent(eventIdn="prepare_injecting_data", connectorIdn="system", global=True)}}

{{StopNotInterruptibleBlock()}}
```

### Guidance code example: call transfer skill

This Guidance skill demonstrates deeper conditional logic, string concatenation with `Concat()`, and command dispatch:

```
{{Set(name="transfer_call_phone_number", value=Stringify(
    GetValueJSON(obj=transfer_call_phone_number_and_clarification_instruction, key="transfer_call_phone_number")
))}}

{{#if transfer_call_phone_number == "null"}}
    {{Set(name="clarification_instruction", value=Stringify(
        GetValueJSON(obj=transfer_call_phone_number_and_clarification_instruction, key="clarification_instruction")
    ))}}

    {{log_event(message=Concat(
        "The command to transfer the call was not executed. Phone number not determined. Clarification: '",
        clarification_instruction, "'"
    ))}}

    {{Return()}}
{{/if}}

{{#if voice_integration_service == "NEWO Voice Integration"}}
    {{SendCommand(
        commandIdn="transfer_call",
        integrationIdn="newo_voice",
        connectorIdn="newo_voice_connector",
        phoneNumber=transfer_call_phone_number,
        waitForTransferSeconds=transfer_call_timeout
    )}}
{{/if}}
```

## NSL vs Guidance comparison

The following side-by-side comparison shows how equivalent logic looks in each format.

**Variable assignment:**

| **NSL (`.nsl`)**                             | **Guidance (`.nslg`)**                               |
| -------------------------------------------- | ---------------------------------------------------- |
| `{% set user_id = GetUser().id \| string %}` | `{{Set(name="user_id", value=GetUser(field="id"))}}` |

**Conditional:**

| **NSL (`.nsl`)**                        | **Guidance (`.nslg`)**             |
| --------------------------------------- | ---------------------------------- |
| `{% if flag == "True" %}...{% endif %}` | `{{#if flag == "True"}}...{{/if}}` |

**LLM prompt construction:**

| **NSL (`.nsl`)**          | **Guidance (`.nslg`)**              |
| ------------------------- | ----------------------------------- |
| `{{system}}...{{end}}`    | `{{#system~}}...{{~/system}}`       |
| `{{assistant}}...{{end}}` | `{{#assistant~}}...{{~/assistant}}` |

**LLM generation call:**

| **NSL (`.nsl`)**                          | **Guidance (`.nslg`)**                               |
| ----------------------------------------- | ---------------------------------------------------- |
| `{% set result = Gen(temperature=0.2) %}` | `{{Set(name="result", value=Gen(temperature=0.2))}}` |

**String concatenation:**

| **NSL (`.nsl`)**             | **Guidance (`.nslg`)**             |
| ---------------------------- | ---------------------------------- |
| `"Hello " ~ user_name ~ "!"` | `Concat("Hello ", user_name, "!")` |

**Comments:**

| **NSL (`.nsl`)**          | **Guidance (`.nslg`)**                             |
| ------------------------- | -------------------------------------------------- |
| `{# This is a comment #}` | `{{!-- This is a comment --}}` or `{{! comment }}` |

## Runner type and file extension matching

Each skill in a flow YAML specifies a `runner_type` that must match the file extension of the referenced `prompt_script`. The flow also declares a `default_runner_type` that applies when a skill does not specify its own.

| **`runner_type`** | **File extension** | **Language**                        |
| ----------------- | ------------------ | ----------------------------------- |
| `nsl`             | `.nsl`             | Newo Script Language (Jinja2-based) |
| `guidance`        | `.nslg`            | Guidance V1 (Handlebars-based)      |

Example from a flow YAML showing both runner types in the same flow:

```yaml
skills:
  - idn: HandleUserMessageSkill
    prompt_script: flows/MyFlow/skills/HandleUserMessageSkill.nsl
    runner_type: nsl
    model:
      model_idn: gemini25_flash
      provider_idn: google
    parameters: []

  - idn: ConversationStartedSkill
    prompt_script: flows/MyFlow/skills/ConversationStartedSkill.nslg
    runner_type: guidance
    model:
      model_idn: gemini25_flash
      provider_idn: google
    parameters: []

# ...

default_runner_type: guidance
```

:::
**:bangbang: IMPORTANT**
The `runner_type` and file extension must always match. Setting `runner_type: nsl` for a `.nslg` file (or vice versa) will cause a script execution error. When adding a new skill, verify both the file extension and the `runner_type` field in the flow YAML.
:::

## Output expressions and text flow

Understanding how text moves through an NSL script is important for writing effective prompts.

### Text that reaches the LLM

Any text placed between role tags (`{{system}}...{{end}}`, `{{assistant}}...{{end}}`) becomes part of the LLM prompt. This includes:

* **Literal text** --- Written directly between the tags
* **Resolved expressions** --- `{{ variable }}` or `{{ function() }}` expressions that produce string output
* **Whitespace** --- Indentation and newlines between tags are preserved in the prompt

For example, in this script, the entire block between `{{system}}` and `{{end}}` becomes the system message, with `{{scenarios}}` and `{{transcript}}` replaced by their runtime values:

```
{{system}}
You are analyzing a session.

<Scenarios>
{{scenarios}}
</Scenarios>

<ConversationHistory>
{{transcript}}
</ConversationHistory>
{{end}}
```

### Text that does not reach the LLM

* **`{% %}` control blocks** --- `set`, `if`, `for`, and other control structures produce no output themselves
* **`{# #}` comments** --- Entirely stripped
* **Action calls outside role tags** --- Expressions like `{{SetState(...)}}` or `{{SendSystemEvent(...)}}` execute their side effects but their return values (usually `None`) do not contribute meaningful text

### The `Return()` action

`Return()` ends skill execution and optionally passes a value back to the caller. It can appear anywhere in the script:

```
{# Early return with no value #}
{% if not transcript.strip() %}
    {{Return()}}
{% endif %}

{# Return with a computed value #}
{{Return(val=json.dumps(valid_bookings, ensure_ascii=False, indent=2))}}
```

When `Return(val=...)` is used, the returned value is available to the calling skill as the result of the sub-skill function call.

## Cross-references

* See **Skills** for the complete skill anatomy, YAML configuration, parameter passing, and execution lifecycle
* See **Flows** for how skills are organized within flows and connected to events
* See **Events and the event system** for details on `SendSystemEvent` and event dispatching
* See **Attributes system** for the `GetCustomerAttribute`, `GetPersonaAttribute`, and `SetPersonaAttribute` functions referenced throughout NSL scripts
* See **Actors personas and users** for the `GetUser()`, `GetActor()`, and `GetActors()` functions