# net.anisota.spell.custom

> Published by [anisota.net](https://lexicon.garden/identity/did:plc:lcieujcfkv4jx7gehsvok3pr)

## Links

- [View on Lexicon Garden](https://lexicon.garden/lexicon/did:plc:lcieujcfkv4jx7gehsvok3pr/net.anisota.spell.custom)
- [Documentation](https://lexicon.garden/lexicon/did:plc:lcieujcfkv4jx7gehsvok3pr/net.anisota.spell.custom/docs)
- [Examples](https://lexicon.garden/lexicon/did:plc:lcieujcfkv4jx7gehsvok3pr/net.anisota.spell.custom/examples)

## Definitions

### `net.anisota.spell.custom`

**Type**: `record`

A user-authored 'spell' — a conditional rule that produces typed effects on the Anisota interface (hiding elements, switching layouts, showing a lockout/alert, redirecting, overriding a setting, and so on). Each spell is a complete, self-contained record so it can be shared by URL and 'learned' (copied) into another user's PDS without depending on the author's other records, mirroring the net.anisota.observatory.layout architecture. A spell is evaluated against a context snapshot (time, activity, player state, location, …); a spell with no conditions is a manual 'mode' that applies whenever it is cast. Whether a spell is currently cast and in what order lives in the separate net.anisota.spell.book index, not here. The effect 'value' is a heterogeneous union whose shape depends on target+type (a string, number, boolean, [start,end] pair, key list, or a config object such as a lockout/alert/sigil/setting-override), so it is left as 'unknown' and validated client-side.

**Key**: `any`

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `name` | `string` | Yes | Display name for this spell |
| `source` | `ref` → `#source` | No |  |
| `effects` | `array` | Yes | The 'then' clauses — what the spell does when it applies. At least one is expected. |
| `trigger` | `ref` → `#trigger` | No | Optional interval trigger. When present the spell fires on a timer rather than continuously; its conditions (if any) then act as a gate on whether the timed firing is allowed. Absent means the spell is condition-driven (continuous). |
| `createdAt` | `string` (datetime) | Yes | When this spell was first crafted |
| `updatedAt` | `string` (datetime) | Yes | When this spell was last modified |
| `conditions` | `array` | No | The 'if' clauses evaluated against the live context. Empty or absent means the spell has no conditions — it applies whenever it is cast (a manual 'mode' spell), or, for an interval-triggered spell, fires on every tick. |
| `description` | `string` | No | Optional description of what this spell does |
| `conditionLogic` | `string` | No | How multiple conditions combine: 'all' (AND, the default) or 'any' (OR). Ignored when there are fewer than two conditions. |

### `net.anisota.spell.custom#effect`

**Type**: `object`

One 'then' clause: a typed effect on a UI surface. Continuously-applied effects flow through the live evaluation; the episodic types (showLockout, showAlert, redirect) are edge-triggered by their controllers. Each effect also carries an undeclared 'value' property — the payload, whose shape depends on target+type: an array of element/button/action/menu keys for the multi-select hide effects; a { rkey, name } layout reference or { rkey, name, keys } for hideElements; an array of word-list snapshots [{ rkey, name, words, strength, targetFeeds }] for activateList — the words are carried inline so a learned spell still works on a PDS where the referenced net.anisota.graph.wordlist record doesn't exist; or a config object for showLockout (message/scope/duration/bypass/…), showAlert (title/message/cooldownMinutes/…), redirect ({ route, cooldownMinutes }), sigilBeforePost ({ source, sigilShape }), overrideSetting ({ id, value }), or warnBefore* ({ action|element, message, gate }). toggleMode carries no value. It is left out of the schema and validated client-side because lexicon 'unknown' means 'any object', which would reject the array and string payloads.

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `type` | `string` | Yes | The specific effect within the target. Valid types depend on the target (see spellService EFFECT_TARGETS). hideButton/showButton/setButtons are legacy observatory effects kept for backward compatibility. |
| `target` | `string` | Yes | The UI surface the effect acts on. |

### `net.anisota.spell.custom#source`

**Type**: `object`

Provenance. An 'original' spell was crafted here; a 'learned' spell was copied from another user, carrying a backlink to its immediate parent (originalUri/originalDid) and to the top of its lineage (rootUri/rootDid), modelled on Bluesky reply parent/root refs so counts can be aggregated across re-shares. A 'preset' spell was seeded from a built-in template.

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `type` | `string` | No |  |
| `rootDid` | `string` (did) | No | DID of the original crafter at the top of the lineage. |
| `rootUri` | `string` (at-uri) | No | AT URI of the first crafter's spell — the top of the lineage — propagated to every descendant. |
| `presetId` | `string` | No | Identifier of the built-in preset a 'preset' spell was seeded from. |
| `originalDid` | `string` (did) | No | DID of the user this spell was learned from. |
| `originalUri` | `string` (at-uri) | No | AT URI of the spell this copy was learned from (its immediate parent). |

### `net.anisota.spell.custom#trigger`

**Type**: `object`

An interval (timer) trigger. The spell fires every everyMinutes minutes rather than reacting continuously to its conditions.

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `type` | `string` | Yes |  |
| `everyMinutes` | `integer` | Yes | Minutes between firings (1 minute to 24 hours). |

### `net.anisota.spell.custom#condition`

**Type**: `object`

One 'if' clause: a variable compared against a value with an operator. Composite conditions (variable 'activitySum' / 'engagementSum') additionally carry a timeframe and a set of metrics to sum. Words-seen conditions (variable 'wordsSeen') additionally carry the word set to watch for and a counting scope. Each condition also carries an undeclared 'value' property — the thing being compared against. Its shape depends on the variable's type (a number, a 'HH:MM' string, a boolean, a two-element ['HH:MM','HH:MM'] array for time 'between', an integer 0-6 for dayOfWeek, a moon-phase / route / modal / date key string, or a { uri, name } object for a feed), so it is deliberately left out of the schema and validated client-side; lexicon 'unknown' means 'any object', which would reject the scalar cases.

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `scope` | `string` | No | Words-seen ('wordsSeen') conditions only: the window the sighting count covers — 'today' (the calendar day, the default) or 'session' (since the app was opened). |
| `words` | `array` | No | Words-seen ('wordsSeen') conditions only: the words to watch for, stored as trimmed lowercase strings. The condition counts posts containing any of these words as they flow through the viewer's feeds, and compares that count against 'value'. |
| `metrics` | `array` | No | Composite conditions only: which metric keys to sum (e.g. 'postsRead', 'postsCreated', 'repliesSent', 'likesReceived'). See spellService COMPOSITE_METRICS. |
| `operator` | `string` | Yes | Comparison operator. Numeric/composite use gt/gte/lt/lte/eq/neq; time uses those plus 'between'; booleans use is/isNot; enumerated types (dayOfWeek, moonPhase, route, modal, feed, handle, date) use eq/neq. |
| `variable` | `string` | Yes | The context variable to test, e.g. 'timeOfDay', 'dayOfWeek', 'currentDate', 'stamina', 'level', 'lightRemaining', 'moonPhase', 'unreadNotifications', 'sessionMinutes', 'postsReadToday', 'activitySum', 'wordsSeen', 'currentRoute', 'activeModal', 'currentFeed', 'currentProfileHandle', 'isOnExpedition'. The full catalog is defined client-side in spellService VARIABLE_GROUPS. |
| `timeframe` | `string` | No | Composite conditions only: the window over which the chosen metrics are summed. |

## Raw Schema

```json
{
  "id": "net.anisota.spell.custom",
  "defs": {
    "main": {
      "key": "any",
      "type": "record",
      "record": {
        "type": "object",
        "required": [
          "name",
          "effects",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 80,
            "description": "Display name for this spell"
          },
          "source": {
            "ref": "#source",
            "type": "ref"
          },
          "effects": {
            "type": "array",
            "items": {
              "ref": "#effect",
              "type": "ref"
            },
            "description": "The 'then' clauses — what the spell does when it applies. At least one is expected."
          },
          "trigger": {
            "ref": "#trigger",
            "type": "ref",
            "description": "Optional interval trigger. When present the spell fires on a timer rather than continuously; its conditions (if any) then act as a gate on whether the timed firing is allowed. Absent means the spell is condition-driven (continuous)."
          },
          "createdAt": {
            "type": "string",
            "format": "datetime",
            "description": "When this spell was first crafted"
          },
          "updatedAt": {
            "type": "string",
            "format": "datetime",
            "description": "When this spell was last modified"
          },
          "conditions": {
            "type": "array",
            "items": {
              "ref": "#condition",
              "type": "ref"
            },
            "description": "The 'if' clauses evaluated against the live context. Empty or absent means the spell has no conditions — it applies whenever it is cast (a manual 'mode' spell), or, for an interval-triggered spell, fires on every tick."
          },
          "description": {
            "type": "string",
            "maxLength": 500,
            "description": "Optional description of what this spell does"
          },
          "conditionLogic": {
            "type": "string",
            "description": "How multiple conditions combine: 'all' (AND, the default) or 'any' (OR). Ignored when there are fewer than two conditions.",
            "knownValues": [
              "all",
              "any"
            ]
          }
        }
      },
      "description": "A user-authored 'spell' — a conditional rule that produces typed effects on the Anisota interface (hiding elements, switching layouts, showing a lockout/alert, redirecting, overriding a setting, and so on). Each spell is a complete, self-contained record so it can be shared by URL and 'learned' (copied) into another user's PDS without depending on the author's other records, mirroring the net.anisota.observatory.layout architecture. A spell is evaluated against a context snapshot (time, activity, player state, location, …); a spell with no conditions is a manual 'mode' that applies whenever it is cast. Whether a spell is currently cast and in what order lives in the separate net.anisota.spell.book index, not here. The effect 'value' is a heterogeneous union whose shape depends on target+type (a string, number, boolean, [start,end] pair, key list, or a config object such as a lockout/alert/sigil/setting-override), so it is left as 'unknown' and validated client-side."
    },
    "effect": {
      "type": "object",
      "required": [
        "target",
        "type"
      ],
      "properties": {
        "type": {
          "type": "string",
          "description": "The specific effect within the target. Valid types depend on the target (see spellService EFFECT_TARGETS). hideButton/showButton/setButtons are legacy observatory effects kept for backward compatibility.",
          "knownValues": [
            "hideElements",
            "setLayout",
            "showLockout",
            "hideButton",
            "showButton",
            "setButtons",
            "disableAction",
            "warnBeforeAction",
            "hideCornerElement",
            "warnBeforeCornerElement",
            "hideElement",
            "overrideSetting",
            "activateList",
            "hideMenuItem",
            "redirect",
            "sigilBeforePost",
            "scramble",
            "toggleMode",
            "showAlert"
          ]
        },
        "target": {
          "type": "string",
          "description": "The UI surface the effect acts on.",
          "knownValues": [
            "observatory",
            "controlRow",
            "layout",
            "postCard",
            "profileCard",
            "footerControls",
            "settings",
            "wordLists",
            "navigation",
            "composer",
            "interface",
            "theme",
            "alert"
          ]
        }
      },
      "description": "One 'then' clause: a typed effect on a UI surface. Continuously-applied effects flow through the live evaluation; the episodic types (showLockout, showAlert, redirect) are edge-triggered by their controllers. Each effect also carries an undeclared 'value' property — the payload, whose shape depends on target+type: an array of element/button/action/menu keys for the multi-select hide effects; a { rkey, name } layout reference or { rkey, name, keys } for hideElements; an array of word-list snapshots [{ rkey, name, words, strength, targetFeeds }] for activateList — the words are carried inline so a learned spell still works on a PDS where the referenced net.anisota.graph.wordlist record doesn't exist; or a config object for showLockout (message/scope/duration/bypass/…), showAlert (title/message/cooldownMinutes/…), redirect ({ route, cooldownMinutes }), sigilBeforePost ({ source, sigilShape }), overrideSetting ({ id, value }), or warnBefore* ({ action|element, message, gate }). toggleMode carries no value. It is left out of the schema and validated client-side because lexicon 'unknown' means 'any object', which would reject the array and string payloads."
    },
    "source": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string",
          "knownValues": [
            "original",
            "learned",
            "preset"
          ]
        },
        "rootDid": {
          "type": "string",
          "format": "did",
          "description": "DID of the original crafter at the top of the lineage."
        },
        "rootUri": {
          "type": "string",
          "format": "at-uri",
          "description": "AT URI of the first crafter's spell — the top of the lineage — propagated to every descendant."
        },
        "presetId": {
          "type": "string",
          "maxLength": 64,
          "description": "Identifier of the built-in preset a 'preset' spell was seeded from."
        },
        "originalDid": {
          "type": "string",
          "format": "did",
          "description": "DID of the user this spell was learned from."
        },
        "originalUri": {
          "type": "string",
          "format": "at-uri",
          "description": "AT URI of the spell this copy was learned from (its immediate parent)."
        }
      },
      "description": "Provenance. An 'original' spell was crafted here; a 'learned' spell was copied from another user, carrying a backlink to its immediate parent (originalUri/originalDid) and to the top of its lineage (rootUri/rootDid), modelled on Bluesky reply parent/root refs so counts can be aggregated across re-shares. A 'preset' spell was seeded from a built-in template."
    },
    "trigger": {
      "type": "object",
      "required": [
        "type",
        "everyMinutes"
      ],
      "properties": {
        "type": {
          "type": "string",
          "knownValues": [
            "interval"
          ]
        },
        "everyMinutes": {
          "type": "integer",
          "maximum": 1440,
          "minimum": 1,
          "description": "Minutes between firings (1 minute to 24 hours)."
        }
      },
      "description": "An interval (timer) trigger. The spell fires every everyMinutes minutes rather than reacting continuously to its conditions."
    },
    "condition": {
      "type": "object",
      "required": [
        "variable",
        "operator"
      ],
      "properties": {
        "scope": {
          "type": "string",
          "description": "Words-seen ('wordsSeen') conditions only: the window the sighting count covers — 'today' (the calendar day, the default) or 'session' (since the app was opened).",
          "knownValues": [
            "today",
            "session"
          ]
        },
        "words": {
          "type": "array",
          "items": {
            "type": "string",
            "maxLength": 100
          },
          "description": "Words-seen ('wordsSeen') conditions only: the words to watch for, stored as trimmed lowercase strings. The condition counts posts containing any of these words as they flow through the viewer's feeds, and compares that count against 'value'."
        },
        "metrics": {
          "type": "array",
          "items": {
            "type": "string",
            "maxLength": 64
          },
          "description": "Composite conditions only: which metric keys to sum (e.g. 'postsRead', 'postsCreated', 'repliesSent', 'likesReceived'). See spellService COMPOSITE_METRICS."
        },
        "operator": {
          "type": "string",
          "description": "Comparison operator. Numeric/composite use gt/gte/lt/lte/eq/neq; time uses those plus 'between'; booleans use is/isNot; enumerated types (dayOfWeek, moonPhase, route, modal, feed, handle, date) use eq/neq.",
          "knownValues": [
            "gt",
            "gte",
            "lt",
            "lte",
            "eq",
            "neq",
            "between",
            "is",
            "isNot"
          ]
        },
        "variable": {
          "type": "string",
          "maxLength": 64,
          "description": "The context variable to test, e.g. 'timeOfDay', 'dayOfWeek', 'currentDate', 'stamina', 'level', 'lightRemaining', 'moonPhase', 'unreadNotifications', 'sessionMinutes', 'postsReadToday', 'activitySum', 'wordsSeen', 'currentRoute', 'activeModal', 'currentFeed', 'currentProfileHandle', 'isOnExpedition'. The full catalog is defined client-side in spellService VARIABLE_GROUPS."
        },
        "timeframe": {
          "type": "string",
          "description": "Composite conditions only: the window over which the chosen metrics are summed.",
          "knownValues": [
            "today",
            "week"
          ]
        }
      },
      "description": "One 'if' clause: a variable compared against a value with an operator. Composite conditions (variable 'activitySum' / 'engagementSum') additionally carry a timeframe and a set of metrics to sum. Words-seen conditions (variable 'wordsSeen') additionally carry the word set to watch for and a counting scope. Each condition also carries an undeclared 'value' property — the thing being compared against. Its shape depends on the variable's type (a number, a 'HH:MM' string, a boolean, a two-element ['HH:MM','HH:MM'] array for time 'between', an integer 0-6 for dayOfWeek, a moon-phase / route / modal / date key string, or a { uri, name } object for a feed), so it is deliberately left out of the schema and validated client-side; lexicon 'unknown' means 'any object', which would reject the scalar cases."
    }
  },
  "$type": "com.atproto.lexicon.schema",
  "lexicon": 1
}
```
