# HSX syntax

Read [how Hyperscale fits](runtime.md#how-hyperscale-fits) for provider authority and the shared operation API.

HSX source declares a Product's object kinds, the instruments attached to them
and the names callers use to run their actions. This page covers three things a
first program leaves open: public action names, `owner` and `actor`, and money
units.
The [language reference](https://github.com/hyperscale0/hyperscale-hsx/blob/main/docs/README.md)
covers the rest. Every HSX block on this page is a complete program that the
current compiler accepts.

## Declarations at a glance

```hsx
program tutoring "Tutoring studio"
currency SAR
use money

object lesson "Lesson" {
  fields { student: text, startsAt: date }
  columns: [student, startsAt]
  attach payment = money.transfer {
    payer: owner, payee: operator, amount: 150 SAR
    expose create as book_lesson
    expose pay as pay_for_lesson
  }
}
```

| Declaration                               | What it does                                                                                                      |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `program name "Title"`                    | Opens every program. The name is an identifier; the title is display text.                                        |
| `currency SAR`                            | Optional. SAR is the only currency this release accepts, so omitting it means SAR.                                |
| `use header`                              | Imports a standard header such as `money`, `escrow` or `booking`. There are no file imports.                      |
| `party name: business`                    | Declares a business, person or staff party. Each Build binds a declared business to a business customer.          |
| `object kind "Title" { ... }`             | Declares a record kind with `fields`, up to eight `columns`, optional `entryActions` and its attachments.         |
| `attach name = header.instrument { ... }` | Connects an instrument to the object: party bindings, tunables such as `amount`, `expose`, `rename`, `economics`. |
| `instrument name { ... }`                 | Authors your own instrument with fields, a lifecycle and actions.                                                 |
| `expose path as name` and `hide path`     | Program-level control of public action names, by full path such as `lesson.payment.pay`.                          |

## Public action names

Attached actions are private. `expose pay` publishes the action under its own
name, `pay`. `expose pay as pay_for_lesson` publishes it as `pay_for_lesson`.
The public name is what object discovery lists and what `object run` takes.
Exposing an action grants no authority. The instrument's actor rule still
decides who may run it, as the next section shows.

Each public name may appear once on an object. Two attachments of the same
instrument have the same action names, so exposing `pay` from both fails:

```text
attach payment = money.transfer { payer: owner, payee: operator, amount: 150 SAR, expose pay }
attach deposit = money.transfer { payer: owner, payee: operator, amount: 50 SAR, expose pay }
```

The compiler refuses this program and names both attachments, because `lesson`
would list `pay` twice. Two different instruments that both expose `cancel`
fail the same way. Give each one its own public name with `as`:

```hsx
program tutoring "Tutoring studio"
use money

object lesson "Lesson" {
  fields { student: text }
  attach payment = money.transfer {
    payer: owner, payee: operator, amount: 150 SAR
    expose create as book_lesson
    expose pay as pay_lesson
  }
  attach deposit = money.transfer {
    payer: owner, payee: operator, amount: 50 SAR
    expose create as take_deposit
    expose pay as pay_deposit
  }
}
```

`as` renames only the public name. Inside the program you still refer to the
original action: `economics pay { ... }` and the program-level path
`lesson.deposit.pay` both name `pay`, not `pay_deposit`.

A program-level `expose lesson.deposit.pay as pay_deposit` does the same as the
attachment-level line. Its path is object, attachment, action; a child
attachment adds one segment per level, as in
`expose enrolment.tuition.payment.create as create_payment`.
`hide lesson.payment.cancel` removes a public name. Each path appears at most
once across program-level `expose` and `hide`.

`rename { price: salePrice }` inside an attachment is different. It renames an
instrument field so the agreement reads another object field, not an action.

## What a payment is for

`economics` tells the books what a money move is for, so revenue and payouts
add up. Put it inside the attachment and name the action, or a record's action
for a template that creates records, such as `money.schedule`:

```hsx
program bike_club "Bike club"
currency SAR
use money

object member "Member" {
  fields { bike: text }
  attach deposit = money.transfer {
    payer: owner, payee: operator, amount: 50 SAR
    economics pay { purpose: earning, sourceParty: payer }
  }
  attach dues = money.schedule {
    payer: owner, payee: operator, amount: 360 SAR, count: 3
    economics occurrence.pay { purpose: earning, sourceParty: payer }
  }
}
```

Use `earning` when the operator receives the money as revenue,
`participant_payout` when the operator pays a participant, and `pass_through`
when money passes between participants. `sourceParty` names the party the money
comes from. A move with no purpose still passes `hsx check` and `hsx plan`, with
a warning that quotes the exact line to add. The books leave such a payment out
until it has one.

## Owner and actor

An instrument names its parties, such as `payer` and `payee` in
`money.transfer`. The attachment binds each party to someone:

| Binding    | Who it is                                                                                         |
| ---------- | ------------------------------------------------------------------------------------------------- |
| `owner`    | The customer or business that owns the object.                                                    |
| `actor`    | The caller who starts the agreement: a signed-in customer, or the customer named in `onBehalfOf`. |
| `operator` | The company that runs the Product.                                                                |

A customer owns the objects they create. A Product key or member that names a
customer in `onBehalfOf` creates the object for that customer. A Product key
without `onBehalfOf` creates an object the Product owns.

Hyperscale resolves these bindings once, when a caller runs the attachment's
first public action, which creates the agreement. The agreement keeps them.

Who may run an action is a separate rule written in the instrument. In
`money.transfer`, `pay` declares `actor: { party: payer }`: only whoever fills
`payer` may run it. Do not confuse that `actor:` clause, which names a party,
with the `actor` binding, which names the caller. The binding decides who
`payer` is; the clause decides that `payer` runs `pay`.

Bind to `owner` when the person the record belongs to takes part. The first
program on this page binds `payer: owner`. A lesson belongs to the student, and
the student pays. Only the lesson's owner can run `pay_for_lesson`, from their
own session or through a Product key with `onBehalfOf` naming them. The 150 SAR
leaves the owner's account.

Bind to `actor` when someone other than the owner takes part. A seller owns the
car and a buyer starts the purchase:

```hsx
program cars "Car market"
use escrow

object car "Car" {
  fields { make: text, model: text }
  entryActions: [start_purchase]
  attach sale = escrow.hold {
    payer: actor, payee: owner
    expose create as start_purchase
    expose fund as pay_for_car
  }
}
```

The buyer who runs `start_purchase` becomes `payer`. From then on only that
buyer can run `pay_for_car`, which moves the price from the buyer's account
into the hold. The seller, as `payee`, runs `deliver` and the other `payee`
actions once the program exposes them.

A party cannot pay itself. `hsx check` and `hsx plan` refuse a move whose
`payer` and `payee` resolve to the same party, and name both bindings. In
`money.schedule` the payee runs `create`, so `payer: actor, payee: owner`
makes the owner both sides of every piece. Bind `payer: owner, payee: operator`
for a customer paying the company. When only the run decides, as with
`payer: actor, payee: operator` on an action the company may start itself, the
check passes with a warning.

### Schedules

`money.schedule` takes money from `payer` to `payee` in pieces. The payee runs
`create` and only the payer runs `activate`. Each piece pays on its due date
while the schedule is active. The payer runs `cancel`, and the payee runs
`stop`; either one ends the pieces still waiting.

Bind `every` for a recurring price. Each piece charges `amount`, the first at
the `startsAt` that `create` takes, and the next one each `every` after it.
`create` refuses a `startsAt` in the past.
`every` takes days, weeks, months or years: `1 month`, `2 weeks`, `1 year`,
`7d`. A month steps by the calendar and clamps to the month's end, so a plan
that starts on 31 January charges on 28 or 29 February and 31 March. Paying a
piece creates the next, so only one piece waits ahead. Without `count` the
plan runs until it is stopped or cancelled; `count: 6` ends it after six
pieces. A plan that falls behind, because the payer activated it late or the
sandbox clock jumped years ahead, catches up in transactions of at most 126
pieces each, one after another.

```hsx
program gym "Gym"
use money

object membership "Membership" {
  fields { plan: text }
  attach dues = money.schedule {
    payer: owner, payee: operator, amount: 250 SAR, every: 1 month
    economics occurrence.pay { purpose: earning, sourceParty: payer }
    expose create as start_membership
    expose activate as accept_membership
    expose stop as end_membership
  }
}
```

Leave out `every` for a fixed set of dates. `create` then takes `dates`, one
per `count`, and the pieces sum to `amount`. Once a schedule is cancelled or
stopped, `schedule list` shows its waiting piece as `cancelled`. A cancelled
piece never runs and drops out of the totals.

### Deposits and late fees

`money.hold` holds a deposit from `payer` until `release` pays it to `payee`
or `refund` returns it. The `released_by` tunable names who runs `release`.
It defaults to `payee`, so a shop bound as `payee: operator` keeps a deposit
from its own session, with no `onBehalfOf`. Bind `released_by: payer` for an
escrow-style hold that only the payer releases. Only the payee refunds.

`money.late_fee` charges a fixed `amount` from `payer` to `payee` for each
whole `period` a due time stays unmet, after `grace`, up to `cap`. `period`
defaults to `1d` and `grace` to `1h`. Rename `dueAt` to the record's own due
field. The clock runs each charge, and the payee runs `stop` when the
obligation is met. Stop keeps what was owed: each charge due at or before the
stop still posts, and the fee reads `settling` until the last of those posts,
then `stopped`. A period still running at the stop is not charged. The payee
runs `waive` to forgive what is still owed. A camera due back on the 12th with one day of grace and a
daily 50 SAR fee charges on the 14th, 15th and each later day until it is
returned or the charges reach the cap. The last charge takes only what is left
under the cap. When the payee is the company, `books report` counts each charge
as revenue.

```hsx
program camera_rentals "Camera rentals"
use money

object rental "Camera rental" {
  fields { camera: text, returnBy: date }
  attach deposit = money.hold {
    payer: owner, payee: operator, amount: 1000 SAR
    expose create as agree_deposit
    expose fund as pay_deposit
    expose release as keep_deposit
    expose refund as return_deposit
  }
  attach late = money.late_fee {
    payer: owner, payee: operator, amount: 50 SAR, cap: 300 SAR, period: 1d, grace: 1d
    rename { dueAt: returnBy }
    expose create as agree_late_fees
    expose stop as record_return
    expose waive as waive_late_fees
  }
}
```

A charge the payer cannot fund is refused and shows as clock work. Charging
waits until it is retried, and a return recorded in the meantime keeps the
charges owed at that moment. If the customer never funds them, the shop runs
`waive_late_fees` to end the fee. Percentage and interest-style late fees are not
available.

A customer can reach an object they own or one where they already fill a
party. `entryActions` also admits any active customer of the Product to the
listed create actions, so a buyer can start a purchase on a car they do not
own. An entry action must expose a create whose acting party is bound to
`actor`.

A Product key that sends no `onBehalfOf` acts for the company. It is then the
`actor`, and its moves draw on the Product pool. See
[Product actions](runtime.md#product-objects-and-actions).

## Money

This release supports one currency, SAR. `currency SAR` is optional, and any
other currency, in the declaration or on an amount, fails compilation.

HSX amounts are written in riyals with the currency: `150 SAR` or
`150.50 SAR`, with at most two decimal places. The compiler stores them as
integer minor units, halalas, in the UDL contract: `150.50 SAR` becomes
`"15050"`. Percentages such as `2.5%` become basis points. Fees and tax round
down.

The CLI and the Launch portal take riyals, the way people type them. The CLI
converts object fields, action input and scenario amounts to halalas before it
calls the API. REST and MCP take halalas, as minor-unit strings.

| Where                                                | Unit               | 150 SAR is written    |
| ---------------------------------------------------- | ------------------ | --------------------- |
| HSX literal                                          | Riyals, with `SAR` | `150 SAR`             |
| Object fields, `object create --fields`              | Riyals             | `{"price":"150.00"}`  |
| Action input, `object run --input`                   | Riyals             | `{"amount":"150.00"}` |
| Scenario files, customer `fund` and step `amount`    | Riyals             | `"fund": "150.00"`    |
| `fund <account> <amount>` CLI command                | Riyals             | `150` or `150.00`     |
| Launch portal Add test money                         | Riyals             | `150`                 |
| REST and MCP bodies, `sandbox.account.fund` included | Halalas, string    | `"amount": "15000"`   |

The CLI refuses an amount with more than two decimal places. REST and MCP
refuse a JSON number such as `15000`; send the string. A required money
field with no fixed value becomes input to the action that creates the
agreement. Leave `amount` unset on the lesson and each booking supplies its own
price:

```hsx
program tutoring "Tutoring studio"
use money

object lesson "Lesson" {
  fields { student: text }
  attach payment = money.transfer {
    payer: owner, payee: operator
    expose create as book_lesson
    expose pay as pay_for_lesson
  }
}
```

These commands use an installed CLI (`npm i -g @hyperscale0/cli`); without the
install, `bunx @hyperscale0/cli@latest <command>` runs the same commands.

```sh
hyperscale fund <customer-account-id> 150
hyperscale object create lesson --fields '{"student":"Noura"}' --on-behalf-of <customer-id> --json
hyperscale object run lesson <object-id> book_lesson --input '{"amount":"150.00"}' --on-behalf-of <customer-id> --json
hyperscale object run lesson <object-id> pay_for_lesson --on-behalf-of <customer-id> --json
```

`fund` adds 150 riyals. `book_lesson` sets the price to 150.00 riyals, which
the CLI sends as 15000 halalas, and `pay_for_lesson` moves it. In the car
market, the escrow reads the price from the object, so it is an object field:
`object create car --fields '{"make":"Toyota","model":"Camry","price":"95000.00"}'`
lists a car at 95,000 SAR.

## Refusals

A refused `requires` comparison comes back as `state_conflict`, HTTP 409, with
`details.reason` set to `requirement_failed`. The message names the field and
the rule, for example `price must equal deposit + balance`, and
`details.nextStep` says what to send. A value appears only when the caller sent
it in the same request: action input, subject fields sent with the action, or
fields the request set. A value stored earlier on the object or agreement, an
account balance or a `sensitive` field never appears in the message, the error
details, the receipt or the audit log. A wrong voucher code reads
`guess must equal code (sent GUESS-1)`, never the stored code.
