Advanced Verb Patterns
Patterns that build on the basics in Creating MOO Verbs: calling
other verbs, parent dispatch with passthrough(), helper verbs that
return values, async and time-budget handling, spatial placement, and
the rest of the moo.sdk toolkit.
Calling other verbs
The Django ORM doesn’t honour MOO inheritance, so Object provides
helper methods that walk the parent chain:
obj.invoke_verb("announce", "broadcast text")
# Or via __getattr__:
obj.announce("broadcast text")
__getattr__ first looks for a verb by that name, then falls through
to property lookup. Each call counts against the calling task’s 3-second
time limit.
A real verb that demonstrates the pattern is
default/verbs/thing/take.py:
elif this.moveto(context.player):
this.clear_placement()
print(this.take_succeeded_msg(title))
if msg := this.otake_succeeded_msg(title):
this.location.announce(msg)
Three verbs are invoked here without explicit imports:
this.moveto(context.player)runs themovetoverb onthis(or an inherited one from a parent class). It returns a truthy value on success.this.take_succeeded_msg(title)is a helper verb that returns a formatted string — see “Helper verbs that return values” below.this.location.announce(msg)runs theannounceverb on the room.
passthrough() for parent dispatch
passthrough is the MOO equivalent of super(). It calls the same
verb on the next ancestor up the parent chain, so a child can run
type-specific logic and then defer to the generic behaviour.
default/verbs/thing/moveto.py is the reference:
#!moo verb moveto --on $thing
# pylint: disable=return-outside-function,undefined-variable
where = args[0]
# Clear placement for any objects placed on this one in the current room.
# Runs before the move so this.location is still the source room.
if this.location:
for placed in list(this.placed_objects.filter(location=this.location).all()):
placed.clear_placement()
if where.is_unlocked_for(this):
return passthrough(where)
return False
The thing-specific work — clearing placements of objects sitting on this
one — runs first, then passthrough(where) defers to
$root_class.moveto, which performs the actual database move. If the
lock check fails, the verb returns False without calling the parent.
Pass through any args/kwargs you received; the parent verb’s
signature has no idea which child invoked it:
return passthrough(*args, **kwargs)
Helper verbs that return values
The “return values are discarded” warning in Creating MOO Verbs
applies only to verbs invoked from the command parser. Verbs invoked
as methods — obj.foo() — return values normally. Most non-trivial
default verbs use a layer of helpers that return strings to be
print()ed by the parser-facing verb.
default/verbs/thing/messages.py defines eight such helpers in one
file:
#!moo verb otake_succeeded_msg otake_failed_msg take_succeeded_msg take_failed_msg odrop_succeeded_msg odrop_failed_msg drop_succeeded_msg drop_failed_msg --on $thing
# pylint: disable=return-outside-function,undefined-variable
prop_value = this.get_property(verb_name)
title = this.title()
prop_value = prop_value.replace("%T", title.capitalize()).replace("%t", title)
return _.string_utils.pronoun_sub(prop_value)
The shebang lists eight aliases, and the body uses verb_name to read
the matching property. take.py then calls
this.take_succeeded_msg(title), gets a string back, and print()s
it. The format-code substitution and pronoun rewrite are concentrated
in one place rather than duplicated across take.py, drop.py, and
their failure paths.
This is the right shape for any “build a string that another verb will display” helper.
Asynchronous and delayed execution with invoke()
moo.sdk.invoke() enqueues a verb for later execution as its own
Celery task. Each invocation gets a fresh 3-second budget.
- moo.sdk.invoke(*args, verb=None, callback=None, delay=0, periodic=False, cron=None, _caller=None, _player=None, **kwargs)
Asynchronously execute a Verb, optionally returning the result to another Verb. This is often a better alternative than using __call__-syntax to invoke a verb directly, since Verbs invoked this way will each have their own timeout.
- Parameters:
verb (Verb) – the Verb to execute
callback (Verb) – an optional callback Verb to receive the result
delay (
int) – seconds to wait before executing, cannot be used with cronperiodic (
bool) – should this task continue to repeat? cannot be used with croncron (
str|None) – a crontab expression to schedule Verb execution_caller – explicit caller override; falls back to
context.caller. Used bytransaction.on_commitcallbacks that fire after theContextManagerhas exited and cleared the contextvars._player – explicit player override; falls back to
context.player. Same reason as_caller.args – positional arguments for the Verb, if any
kwargs – keyword arguments for the Verb, if any
- Returns:
a
PeriodicTaskinstance or None if the task is a one-shot- Return type:
Optional[
PeriodicTask]
Use cases:
Delayed execution — schedule a verb to run after a delay (
delay=Nseconds).Periodic execution —
periodic=Truekeeps re-firing on the same interval. Wizard-only.Cron schedules —
cron="<expression>"for time-of-day style triggers. Wizard-only.Time-aware continuation — hand off remaining work to a fresh task before the current task’s budget runs out (see below).
from moo.sdk import invoke, context
# Run the same verb again 30 seconds from now.
invoke(context.parser.verb, delay=30)
For periodic tasks that should also re-use existing verb code, pass a callback verb:
from moo.sdk import invoke, context
if context.parser is not None:
say = context.caller.get_verb("say", recurse=True)
invoke(verb=context.parser.verb, callback=say, delay=30, value=0)
return
value = kwargs["value"] + 1
return f"A parrot squawks {value}." # passed to the callback verb
Time-aware continuation
Each verb invocation, including synchronous calls to other verbs, must finish within the configured task time limit (3 seconds by default). For loops over many objects, check the remaining budget and hand the unfinished work to a fresh task before being killed.
task_time_low() returns True when the current task is near its
limit; schedule_continuation() re-invokes the verb with the remaining
work as args[0].
from moo.sdk import context, task_time_low, schedule_continuation
Inside the loop, dispatch on verb_name so a single file handles both
the parser entry point and the continuation re-entry:
if task_time_low():
schedule_continuation(items[i:], this.get_verb("my_batch"))
return
default/verbs/programmer/at_reload.py is the canonical
implementation. The relevant excerpt:
#!moo verb @reload reload_batch --on $programmer --dspec any --ispec on:any
def do_reload_batch(verbs):
count = 0
for i, verb in enumerate(verbs):
if task_time_low():
schedule_continuation(
verbs[i:],
this.get_verb("reload_batch"),
msg=f" Time limit approaching; continuing in a new task ({len(verbs) - i} verb(s) remaining)...",
)
return True, count
context.player.tell(f" Reloading {verb}...")
verb.reload()
count += 1
return False, count
if verb_name == "reload_batch":
# Continuation entry: args[0] is a list of Verb PKs from the prior task.
verbs = list(Verb.objects.filter(pk__in=args[0]))
continued, count = do_reload_batch(verbs)
if not continued:
context.player.tell(f"Reloaded {count} verb(s).")
else:
# Parser entry: gather the work, kick off the loop.
...
Things to keep in mind:
Use
context.player.tell()(notprint()) for progress messages.tell()delivers immediately;print()buffers until the verb returns, so the player wouldn’t see anything until the very end.Materialise the queryset with
list()before the loop so the database cursor isn’t held open across the time check.Define a separate alias (
reload_batch) for the continuation entry point. Dispatch onverb_name, not onargs[0]’s type.Keep continuation args minimal — the list of remaining work is enough; progress was already delivered via
tell().Never assign to a local named
verb_name. Python scoping makes the whole function treat it as a local, and reads before the assignment raiseUnboundLocalError. See the gotcha in Creating MOO Verbs.
Returning a value from a verb
return may appear at any depth in verb code (not just at function
end), thanks to RestrictedPython’s compilation. Use it for two things:
Helper verbs that build strings or compute state for another verb to consume (see “Helper verbs that return values” above).
Early exits in command verbs — bare
return, no value. The string version is silently discarded:
if not args:
print(f"Usage: {verb_name} <object_name>")
return # exits cleanly
if not args:
return f"Usage: {verb_name} <object_name>" # WRONG — player sees nothing
Placement verbs
Objects can be placed in a spatial relationship to another object in the same room. Two fields on the Object record the placement:
placement_prep— preposition string ("on","under","behind","before","beside","over").placement_target— the Object it is placed relative to.
PLACEMENT_PREPS (from moo.sdk) lists every valid preposition.
HIDDEN_PLACEMENT_PREPS is the subset ("under", "behind") whose
placed items don’t appear in the room’s contents listing and aren’t
findable by name through the parser.
Placement is cleared automatically when an object is taken, dropped,
or moved. If the placement target is deleted, both fields go to
None.
Writing a placement verb
#!moo verb place --on $thing --dspec this --ispec on:any --ispec under:any --ispec behind:any --ispec before:any --ispec beside:any --ispec over:any
from moo.sdk import context, NoSuchPropertyError, UsageError, PLACEMENT_PREPS
prep = None
for p in PLACEMENT_PREPS:
if context.parser.has_pobj_str(p):
prep = p
break
if prep is None:
raise UsageError(f"Usage: place <object> {'/'.join(PLACEMENT_PREPS)} <target>")
target = context.parser.get_pobj(prep)
# Optional: check surface_types restriction on the target
try:
allowed = target.get_property("surface_types")
if prep not in allowed:
print(f"You can't place things {prep} the {target.title()}.")
return
except NoSuchPropertyError:
pass
this.set_placement(prep, target)
print(f"You place {this.title()} {prep} the {target.title()}.")
context.player.location.announce(
f"{context.player.title()} places {this.title()} {prep} the {target.title()}."
)
Key points:
--ispecmust enumerate every supported preposition; there is no wildcard form.set_placement(prep, target)is atomic: it sets both fields and saves in one call.clear_placement()removes placement without touching other fields.
Reading placement
placement = this.placement # (prep, target) or None
if placement is None:
print(f"The {this.title()} is not placed anywhere.")
else:
prep, target = placement
print(f"The {this.title()} is {prep} the {target.title()}.")
Restricting surface types
Set the surface_types property on a target to limit which prepositions
it accepts:
desk.set_property("surface_types", ["on", "beside"])
# Now: "place book on desk" succeeds; "place book under desk" fails.
If surface_types is absent, all placement prepositions are accepted.
Common SDK helpers
moo.sdk exposes more than the basics covered in
Creating MOO Verbs. The functions you’ll reach for most often
beyond lookup/create/write:
Tasks:
invoke,cancel_scheduled_task,get_scheduled_task_info,task_time_low,schedule_continuation,set_task_perms,invoked_verb_name.Full-screen UIs:
open_editor,open_paginator,can_open_editor.Players:
players,connected_players,owned_objects,owned_objects_by_pks,ensure_player_record,remove_player_record.Client capabilities:
get_client_mode,get_wrap_column,get_session_setting,set_session_setting.Out-of-band protocols:
send_gmcp,play_sound,room_info_payload— see Accessibility and MUD Client Compatibility for the protocol story.Admin:
boot_player,server_info.Mail:
send_message,get_mailbox,count_unread, etc.
For the full inventory with signatures and arguments, see SDK Functions.
Editor callback example
default/verbs/note/edit.py opens an editor pre-filled with the
note’s body, and routes the saved text back into a callback verb:
from moo.sdk import context, open_editor
existing = this.get_property("body", default="")
open_editor(context.player, existing, this.set_body)
this.set_body is another verb on the note that takes the saved text
as its first argument and writes it to the body property. The editor
runs in the SSH process; the callback fires as a fresh Celery task
once the player saves.
Mode-aware verbs
When a verb would open a TUI, branch on get_client_mode() so MUD
clients and screen-reader users still get usable output:
from moo.sdk import get_client_mode, open_editor
if get_client_mode() == "raw":
print(f"Use: @edit description with \"your text here\"")
return
open_editor(context.player, existing_text, this.set_description)
See Accessibility and MUD Client Compatibility for the full mode-selection model.
Verb time limits
Each verb invocation, including synchronous calls to other verbs,
finishes within CELERY_TASK_TIME_LIMIT (default 3 seconds) or the
worker kills it. For longer work:
Compose into multiple verb invocations via
invoke()— each gets its own 3-second budget.Use the time-aware continuation pattern above for loops over many items.
For wall-clock waits (“happen 30 seconds from now”), use
invoke(verb, delay=30)rather than blocking inside a verb.
Where to go next
Creating MOO Verbs — basics: shebang, parser, output, errors, properties.
SDK Functions — full SDK function reference.
The DjangoMOO Runtime —
contextattributes,task_time.Command Parser Reference — verb search order, preposition synonyms.
Verb Sandbox Security Reference — RestrictedPython model and the underscore/format/QuerySet guards.