actions/
Me keeps this legacy host configuration outside Realm execution. Use the captured handler migration for Me Realms.
Action specifications — YAML files that define executable operations. The host’s planner picks them by their declared input / output types and runs them as GOAP actions. The stepType discriminator selects the shape; the framework’s NameOrClassTypeIdResolver resolves either a registered short name (e.g. action, goal) or a fully-qualified class name (com.example.MyCustomActionSpec) to an ActionSpec class on the classpath. Host extensions can use either path.
stepType value |
Shape |
|---|---|
action |
Framework PromptedActionSpec — typed LLM call producing outputTypeName |
<FQN> |
Any other ActionSpec subtype on the classpath. Use this when shipping a host-extension shape whose YAML contract isn’t yet stable enough to claim a short name. |
Short name vs FQN dispatch. A short name like action is a public contract; once realm authors write it, you can’t change the spec’s shape without breaking their YAML. Reserve short names only for shapes that have stabilised. FQN dispatch lets a host iterate freely on field names, parsing, and dispatch semantics without committing to a YAML slot upstream. The flip side: an FQN that appears in published realm YAML is itself a public identifier — a host that moves or renames the class must keep the old name resolvable, or every realm that wired against it breaks.
Example host extension (the assistant’s predicate-driven PolicyActionSpec):
stepType: com.embabel.world.policy.spec.PolicyActionSpecname: email_unreplieddescription: A thread you're a participant in has activity from someone else, ≥ 24h agoinputTypeNames: - email.threadwhenExpr: "$self in participants and last_sender != $self and age >= 24h"cost: 0.01value: 1.0The framework’s resolver calls Class.forName(stepType) and deserializes the rest of the YAML into that class. No registerSubtypes(...) call required, no @JsonTypeName annotation on the class.
stepType: action — typed LLM call
Section titled “stepType: action — typed LLM call”The framework’s general-purpose PromptedActionSpec. The LLM produces a typed object the planner can chain into other actions. nullable: true declares that the LLM may return null (no output) — the planner sees the missing binding and replans.
stepType: actionname: triage-issuedescription: "Triage a GitHub issue"inputTypeNames: - GitHubIssueoutputTypeName: TriagedIssueprompt: | Triage issue #{{gitHubIssue.issue}} in {{gitHubIssue.owner}}/{{gitHubIssue.repo}}.tools: - githubcost: 0.5value: 1.0Fields: name, description, llm (LlmOptions, optional), inputTypeNames, outputTypeName, pre / post (extra preconditions / effects, optional), cost / value (UtilityAI economics, default 0), canRerun (default false), prompt, toolGroups / tools / references (optional), nullable (default false), export (auto-generate a chat-callable goal, default false).
LLM-judged signals — stepType: action producing AttentionVerdict
Section titled “LLM-judged signals — stepType: action producing AttentionVerdict”LLM-judged attention rules are vanilla stepType: action PromptedActionSpecs whose output is an AttentionVerdict (a typed {reason, confidence, tier} value) and which declare nullable: true so the LLM can return null = “skip”. The host provides a generic wrap action that lifts (AttentionVerdict, Signal) to AttentionCandidate, closing the catchup chain.
stepType: actionname: triage_email_attentiondescription: Judge whether an email thread warrants the user's attention
inputTypeNames: - email.thread
outputTypeName: AttentionVerdictnullable: true
prompt: | Judge whether the thread is worth {{user}}'s attention. Return a verdict if so; return null to skip.
Thread: {{emailThread}}
cost: 0.5value: 1.0The wrap is a two-step GOAP chain per signal: the PromptedActionSpec emits an AttentionVerdict, then the host’s wrap action emits the final AttentionCandidate. The wrap is keyed off (AttentionVerdict, Signal) inputs so it composes generically with any LLM-judgment realm — realm-email, realm-slack, realm-calendar, etc. — without needing per-signal-type wrap classes.
Deterministic rules — host-extension via FQN
Section titled “Deterministic rules — host-extension via FQN”The assistant ships an in-tree PolicyActionSpec for cheap, no-LLM rules over a single signal — fires when its predicate matches and writes an AttentionCandidate. No surface: block — how the candidate gets rendered into a notification is a downstream concern, not the producer’s call.
The YAML uses FQN dispatch (the predicate DSL is still iterating, so we don’t reserve a short stepType slot upstream yet):
stepType: com.embabel.world.policy.spec.PolicyActionSpecname: pr_review_overduedescription: A review was requested from you and it's been over 48h
inputTypeNames: - github.pr_review_request
whenExpr: "reviewer == $self and age >= 48h"
# Same top-level cost / value as PromptedActionSpec. Cheap predicate:# low cost, standard value — the planner picks this before any LLM# judgment on the same signal type.cost: 0.01value: 1.0whenExpr — the predicate DSL
Section titled “whenExpr — the predicate DSL”A boolean expression over the matched signal’s fields. Parser source: com.embabel.world.policy.PolicyExprParser. Grammar (today; expect additions as concrete rules need them):
Operators, lowest to highest precedence:
| Operator | Meaning |
|---|---|
or |
logical OR |
and |
logical AND |
not |
logical NOT (prefix) |
==, != |
equality / inequality |
<, <=, >, >= |
numeric or duration comparison |
in |
membership in a collection (field in collection) |
Literals:
| Form | Type |
|---|---|
"..." |
string (\ escapes the next character) |
42, 3.14 |
number |
true, false |
boolean |
30s, 5m, 48h, 7d, 2w |
duration (seconds / minutes / hours / days / weeks) |
Identifiers — dotted paths walk the signal’s fields. signal.subject, source.url, reviewer, last_sender. Whatever properties the inputTypeNames DomainType declares is reachable. Unresolvable paths fail the comparison (logged at debug, not a parse error) so a misspelled field surfaces as “rule never fires” rather than a load-time crash.
Special tokens:
$self— the user’s identity bundle (username + configured aliases — emails, github handle, etc., fromassistant.policy.self.<username>inapplication.yml). Use in==,!=, or as the LHS ofin.reviewer == $self,$self in participants.age— derivednow - signal.occurredAtas aDuration. Compare against a duration literal:age >= 24h,age < 5m.
Grouping: () for explicit precedence. not (a or b).
Worked examples:
reviewer == $self and age >= 48h$self in participants and last_sender != $self and age >= 24hmentioned == $self and not acknowledgedsource.kind == "email" and message_count > 1Errors at parse time include source span and the unexpected token — realm authors see the column the parser tripped on.
How policies and LLM-judgment actions compose
Section titled “How policies and LLM-judgment actions compose”For each fresh signal of type T, the host runs one AgentProcess whose goal is “an AttentionCandidate exists on the blackboard”. Every loaded action whose preconditions match T is a candidate; UtilityAI picks in value-minus-cost order. A cheap deterministic rule ((cost 0.01, value 1.0) → utility 0.99) wins over an LLM stepType: action producing AttentionVerdict ((cost 0.5, value 1.0) → utility 0.5) — so the cheap rule runs first, writes the AttentionCandidate, the goal is satisfied, and the LLM call never happens.
When the cheap policy doesn’t match, its hasRun_<name>=TRUE blocks re-picking; the LLM action still has positive utility; the planner picks it; it produces an AttentionVerdict (or null = skip); if non-null, the host’s wrap action fires; goal satisfied. If the LLM returns null and no other action can produce an AttentionCandidate, the planner terminates naturally with no candidate.
Actions are deployed to the host’s planner on world load.

