---
title: "How-To: Write GessaScript"
description: "Task recipe for authoring behavior with Script Semantic Patch (the normal path) with Script IR as the canonical contract."
engineVersion: v1.0.234
date: 2026-09-28
license: "(c) Gessa, proprietary. Cite with attribution to https://gessa.ai/docs/. Terms: https://gessa.ai/terms/."
canonical: https://gessa.ai/docs/how-to/write-gessascript/
---

# How-To: Write GessaScript

{% version engineVersion="v1.0.232" coordinate="docs-public.v0" /%}

Add behavior to a world by authoring **GessaScript**. The normal authoring path is the **Script Semantic Patch** (typed, intent-level operations applied to the script) *not* hand-writing Script IR and *not* hand-editing generated source. You declare what should change; the backend compiler owns the rest.

This page describes the canonical authoring flow for v1. Source and visual authoring both round-trip through Script IR; generated TypeScript is runtime/debug output, not normal creator truth.

{% proof class="docs.gessascript" registry="docs/cycles/v1-engine-readiness-loop/PROOF_REGISTRY.json" status="pending" %}
This page is verified against the generated Script Semantic Patch, Script Node, and Script SDK references. Runtime behavior still requires Play or publish proof.
{% /proof %}

> Media placeholder: add a capture showing the same script in Visual and GessaScript source, then a Play trace mapped back to the authored node/source span.

## What is canonical: the IR, not its projections

**Script IR is the canonical behavior contract.** It is the single source of truth for what a script does. The two surfaces creators usually look at (**GessaScript source** and the **visual node graph**) are both constrained authoring surfaces over the IR. Generated TypeScript is an execution and debug projection. Editing source or visual means editing the IR underneath; neither surface is allowed to drift into a separate truth.

The IR's root is versioned (an `irVersion` literal), so a script declares which IR contract it conforms to. You never hand-write that IR. Instead you express intent through the semantic patch layer, and the compiler projects it into canonical IR, owning node ids, ports, and layout. See [Explanation: Script IR and GessaScript](../explanation/script-ir-and-gessascript.md).

## The semantic patch is the normal path

Semantic patches are **creator-facing edit operations** derived from `packages/script-semantic-patch`. The backend compiler projects them into canonical Script IR, and the normal Project Graph script-update path persists the result. The catalog is **Tier-2 generated**; the current count and inventory of operations live in the generated file:

- [`docs/spec/generated/script-semantic-patch.md`](../../../spec/generated/script-semantic-patch.md): the operation catalog, packs, and effect tags.
- [`docs/spec/generated/script-node-catalog.md`](../../../spec/generated/script-node-catalog.md): the canonical Script IR node declarations (event, core, and host nodes).
- [`docs/spec/generated/script-sdk.md`](../../../spec/generated/script-sdk.md): the generated Script SDK surface (`ScriptContext` and the `ctx.*` host namespaces).
- [Reference: Script Nodes](../reference/script-nodes.md): the thin pointer page.

{% generated-reference file="docs/spec/generated/script-semantic-patch.md" label="Script Semantic Patch operations" /%}

You apply a patch through the canonical action `project.script.patch.apply`, surfaced to agents as the MCP tool `project_apply_script_semantic_patch`. Each operation carries typed effect tags (`readState`, `writeState`, `durable`, `spawnEntity`, `despawnEntity`, `emitEvent`, `requiresAuthority`) so the authority layer knows exactly what a behavior touches.

Representative operations (read the generated catalog for the full set and each operation's schema): `findEntitiesByTag`, `forEachEntityInResult`, `incrementNumericState`, `declareWinCondition`, `emitCustomEvent`, `spawnEntityFromTemplate`, `despawnEntity`, `attachComponentWithValidatedDefaults`, `wireHudStateBinding`, and the higher-level `addShooterCollectorLoop`, which expands a common gameplay loop from semantic intent alone. Handler operations (`addActionHandler`, `addTriggerHandler`, `addTimerHandler`, `addTickHandler`, `createCustomEventHandler`) attach the entry points behavior runs from.

{% warning severity="critical" title="Do not push raw IR through the normal authoring path" %}
The visual-editor primitives (`addNode`, `connectExec`, `connectData`, `setInlineValue`, `addHostOperation`, and raw graph node ids) are import / visual-editor / test-only. Do **not** send them through `project_apply_script_semantic_patch`. Behavior edits use the typed semantic operations (the `ai_safe` / `mcp_safe` packs); hand-wiring nodes bypasses the contract that lets the compiler own ids, ports, and layout.
{% /warning %}

## The compiler owns ids, ports, and layout

This is the point of the semantic patch. You declare intent (`add a tick handler`, `spawn an entity from a template`, `declare a win condition`); the compiler decides node ids, wires execution and data ports, and lays out the graph. That is why the source and visual graph stay in lockstep, they both re-project from the IR the compiler produced. If you hand-wired nodes, the two surfaces would drift and the IR would no longer be a single authority. Let the compiler do it.

## The authoring recipe

{% recipe id="author-gessascript-semantic" goal="Add behavior through a typed semantic patch and confirm via Play" audience="both" %}

1. **Open or create the script shell.** For an existing script, resolve its `id_or_key`. To create an empty IR-backed shell, use the workbench or `project_create_script`: then do behavior edits with the patch tool, not by editing the shell's source.
2. **Discover the right operation.** Read the [semantic-patch catalog](../../../spec/generated/script-semantic-patch.md) (or call `engine_list_script_patterns` / `engine_get_script_node`) to choose typed operations that lower into Gessa services rather than raw IR.
3. **Express the change as a patch.** Build the operation(s): e.g. add a handler, then `findEntitiesByTag` → `forEachEntityInResult` → `despawnEntity`, then `declareWinCondition`. Keep the patch intent-level.
4. **Apply through the canonical action.** Call `project_apply_script_semantic_patch` (action `project.script.patch.apply`). The compiler projects your intent into canonical IR; the source and visual graph re-project from it; the normal script-update path persists it.
5. **Validate, then Play.** Run the script publish gate and enter Play to confirm the behavior. See [How-To: Publish and Play](publish-and-play.md).

{% /recipe %}

{% ai-context audience="mcp" priority="must" %}
Use `project_apply_script_semantic_patch` for behavior edits; use `project_create_script` only to make an empty IR-backed shell. Choose operations from the `ai_safe` / `mcp_safe` packs. Never send `addNode`, `connectExec`, `connectData`, `setInlineValue`, `addHostOperation`, or raw graph node ids through this path, they are visual-editor/import/test-only. Read effect tags (`writeState`, `spawnEntity`, `requiresAuthority`) to understand what an operation touches before applying it.
{% /ai-context %}

## Related

- [Explanation: Script IR and GessaScript](../explanation/script-ir-and-gessascript.md): why the IR is canonical and source/visual are projections.
- [Reference: Script Nodes](../reference/script-nodes.md): the generated node catalog and SDK surface.
- [How-To: Use MCP Tools](use-mcp-tools.md): discovery-first authoring from an agent.
- [How-To: Publish and Play](publish-and-play.md): validating a behavior change in a Play session.

Status: this page documents the canonical GessaScript authoring flow for v1. The exact operations, node declarations, and SDK methods live in generated references; use runtime proof for runtime claims.
