Commit Methods
Modality contracts are append-only logs of commits. Each commit contains one or more actions, and each action has a method that determines what it does.
GENESIS
Opens a contract. modal contract create writes it as the first commit.
Validators refuse GENESIS in any later commit: it would change state
without taking a step in the model.
POST
The most common method — writes data to a path in this contract's state.
{
"method": "post",
"path": "/users/alice.id",
"value": "12D3KooWAbCdEfGhIjKlMnOpQrStUvWxYz..."
}
After the commit is accepted, formulas on this contract can name that path
(signed_by(/users/alice.id), text_eq, state_exists, …).
Path Types
Paths must end with a known extension:
| Extension | Type | Example Value |
|---|---|---|
.bool | Boolean | true or false |
.text | Text string | "Hello world" |
.num | Number | 100 or 0.5 |
.date | Date | "2024-01-15" |
.datetime | Date and time | "2024-01-15T10:30:00Z" |
.json | JSON object | {"key": "value"} |
.md | Markdown | "# Title\n\nContent..." |
.id | Modality spelling: lowercase, mirrored, type at the end | "imqi74td...aiajaazfab" |
.wasm | WebAssembly | Base64-encoded WASM |
.modality | Rules/formulas | Modality syntax |
MODEL
Sets or replaces the contract's state machine model. Unlike rules, models can be replaced.
{
"method": "model",
"value": "model escrow { initial idle; idle -> funded [DEPOSIT]; funded -> released [RELEASE] }"
}
CLI Usage
modal contract commit --method model \
--value 'model members_only { initial active; active -> active [] }' \
--sign alice
Note: Models define structure but don't enforce security on their own. Use RULE methods to add protection that persists even if the model is replaced.
RULE
Adds a temporal logic constraint to the contract. Rules are accumulated over time and all must be satisfied.
{
"method": "rule",
"path": "/rules/auth.modality",
"value": "always([<+signed_by(/users/alice.id)>] true)"
}
Rules must have paths ending in .modality.
REPOST
Copies a snapshot of another contract's value into this contract so later rules can name it like any other path. It is not a live pointer: later updates on the source do not change this contract until you REPOST again.
{
"method": "repost",
"path": "/reposts/abc123def456/announcements/latest.text",
"value": "Hello from the other contract!",
"source_contract": "abc123def456",
"source_path": "/announcements/latest.text",
"source_commit": "a1b2c3..."
}
path— dest path in this contract (a normal/...path). Default dest is/reposts/<source_id><source_path>. Choose a path like/parties/alice.idwhen the imported value should be that identity.value— bytes copied from the source.source_contract,source_path,source_commit— provenance. The hub checks that the source contract atsource_commithasvalueatsource_path.
After accept, dest is ordinary accepted state. Example: REPOST Alice's key to
/parties/alice.id, then signed_by(/parties/alice.id) works. Method label
is +REPOST (not +POST). modifies(dest) matches. post_to_path stays
POST-only. Models that only allow +POST must also allow +REPOST (or an
unlabeled write loop) before import commits will pass.
Working tree
Default dest files live under reposts/<source_id>/.... Custom dests live
under state/ like POST. modal commit --all emits method: "repost" for
paths staged by modal repost.
CLI Usage
# Default dest /reposts/<source_id>/announcements/latest.text
modal repost abc123def456 /announcements/latest.text
# Custom dest so formulas can say signed_by(/parties/alice.id)
modal repost abc123def456 /parties/alice.id /parties/alice.id
# Source is a local contract directory (no hub)
modal repost abc123def456 /notes/hello.text --from-dir ../source-contract
modal commit --all
CREATE
Creates a new asset in the contract. The asset_id may not contain :,
which separates a creator from an asset id in a held asset's name.
{
"method": "create",
"value": {
"asset_id": "token1",
"quantity": 2100000000000000,
"divisibility": 1,
"decimals": 8
}
}
quantity and every amount are in the asset's smallest unit. Every amount
sent must be a multiple of divisibility. decimals is optional and only
for display: with 8, a wallet shows 150000000 as 1.5.
SEND
Sends assets to another contract.
{
"method": "send",
"value": {
"asset_id": "token1",
"to_contract": "target_contract_id",
"amount": 1000
}
}
A contract sends an asset it created, or one it received. Name a received
asset by its creator in asset_contract; omit it for the contract's own
asset. Naming the contract itself is refused.
An optional memo carries JSON the receiver reads, for example what the
sender wants done with the asset. It is recorded with the SEND.
{
"method": "send",
"value": {
"asset_contract": "creator_contract_id",
"asset_id": "token1",
"to_contract": "pool_contract_id",
"amount": 100,
"memo": {"op": "swap", "min_out": 300}
}
}
RECV
Receives assets from a SEND in another contract. The SEND commit must already be sequenced. When the network requires validator certs, dest apply also needs a prefix-cert supermajority through that SEND commit.
{
"method": "recv",
"value": {
"send_commit_id": "abc123...",
"send_index": 1
}
}
send_index picks which SEND of that commit, counting from 0 (the
default). SENDs a program emitted count too, in the order the invoke
emitted them. Each SEND is received once.
A RECV may also state what it receives: from_contract, asset_contract
and asset_id, amount, and memo. Apply refuses a RECV whose statement
differs from the SEND, so rules can read the statement as the fact. A
statement of asset_id without asset_contract claims the receiver's own
asset.
{
"method": "recv",
"value": {
"send_commit_id": "abc123...",
"from_contract": "trader_contract_id",
"asset_contract": "creator_contract_id",
"asset_id": "token1",
"amount": 100,
"memo": {"op": "swap", "min_out": 300}
}
}
A commit applies whole or not at all: if any action fails, none of its writes are kept.
INVOKE
Executes a WASM program stored in the contract.
{
"method": "invoke",
"path": "/__programs__/calculator.wasm",
"value": {
"args": {
"operation": "add",
"a": 5,
"b": 3
}
}
}