Creating MOO Verbs

This guide is the reference for writing verb code: the file format, the names injected into the verb’s scope, the parser API, the output mechanisms, error handling, and the patterns most verbs share. For the beginner walk-through, see Your First MOO Verb. For patterns beyond the basics — calling other verbs, time-aware continuation, placement, SDK helpers — see Advanced Verb Patterns.

The shebang line

Every verb file starts with a #!moo verb shebang that supplies the verb’s name(s), the object it lives on, and how the parser should match it:

#!moo verb take --on $thing --dspec this --ispec from:any

Grammar:

#!moo verb verb_name1 [verb_name2] ...
    --on object_name
    [--dspec this|any|none|either]
    [--ispec PREP:SPEC [PREP:SPEC ...]]
  • Verb names — space-separated. Multiple names act as aliases; inside the verb body, verb_name is the specific alias the player invoked.

  • --on — required. Accepts a player name (Wizard), an object ID (#5), or a system property reference ($thing, $room, $container).

  • --dspec — direct object specifier:

    • this — verb fires only when the parsed direct object resolves to the object the verb is on. (drop widget matches widget’s drop.)

    • any — a direct object must be present; any string is accepted.

    • either — direct object is optional. this is set correctly when one is given.

    • none (the default) — verb only matches commands with no direct object.

  • --ispec — indirect object specifiers, one per preposition. --ispec on:this --ispec in:this would let put X on Y and put X in Y reach the same verb. Use none when the preposition itself must be present but takes no object (e.g. crawl --dspec none --ispec under:any matches crawl under desk).

Examples from the default verbs:

#!moo verb accept --on $room
#!moo verb take --on $thing --dspec this --ispec from:any
#!moo verb put give --on $thing --dspec this --ispec on:this --ispec in:this
#!moo verb @reload reload_batch --on $programmer --dspec any --ispec on:any

Common --ispec choices

Interaction

--ispec

Sample command

Talking to someone

to:any

talk to barkeep

Sitting / lying down

on:this

sit on couch

Putting items inside

in:any

put bottle in bag

Taking / drinking from

from:this

drink from tap

Examining via

through:any

look through scope

Attacking / aiming

at:this

punch at dummy

Words the parser treats as prepositions

The lexer scans every command for preposition words before it splits the command into parts. Words like from, to, with, in, on, at, and into are always preposition boundaries — even when they appear inside what you intended to be a plain argument.

If a player is going to type something whose argument contains one of those words, they need to quote it:

@eval from moo.sdk import lookup       ← parsed as prep boundary; verb won't match
@eval "from moo.sdk import lookup"     ← preserved as a single argument

@eval pre-imports moo.sdk, so the example above is also avoidable by writing @eval "lookup('Wizard').location.name". The interactive shell expands a leading ; to @eval:

; lookup('Wizard').location.name

The full preposition list lives in settings.PREPOSITIONS and is documented in Command Parser Reference. When a verb takes free-form text (a code snippet, a description, a message), document the quoting expectation in the verb’s help.

RestrictedPython execution

Verb code is compiled and run inside Zope’s RestrictedPython sandbox. Practical implications:

  • Only the modules in settings.ALLOWED_MODULES may be imported: moo.sdk, hashlib, re, datetime, time. Wizards additionally get moo.core.models.{object,verb,property}.

  • Only the builtins in settings.ALLOWED_BUILTINS are available: all, any, dict, enumerate, getattr, hasattr, list, max, min, set, sorted, sum, and PermissionError. Other builtins (type, dir, eval, exec, open, …) are absent by design.

  • Attribute names beginning with _ raise AttributeError. The single exception is the global _ reference to the System Object.

  • str.format and str.format_map are blocked on string instances — use f-strings or str.replace() instead.

  • return may appear at any level of the verb body, not just at function end (RestrictedPython rewrites the source).

For the full sandbox model and the security rationale, see Verb Sandbox Security Reference.

Names injected into the verb’s scope

Every verb is compiled into a function with the signature def verb(this, passthrough, _, *args, **kwargs). Inside the body, the following names are available without import:

Name

Type

Description

this

Object

The object the verb was matched on. With --dspec this, that’s the direct object. With --dspec any or none, it’s the caller. Use context.player for “who is acting” logicthis is not always the caller (see Command Parser Reference).

passthrough

callable

Calls the same verb on the parent class. Pass any arguments through: passthrough(*args, **kwargs).

_

Object

The System Object (pk=1). Used for _.string_utils, _.gripe_recipients, etc.

args

list

Positional arguments when the verb is invoked as a method. Empty when invoked from the command parser.

kwargs

dict

Keyword arguments when invoked as a method. Empty from the parser.

verb_name

str

The exact alias the caller used. Do not assign to a local variable named verb_name — Python scoping makes that a local for the entire function and reads before the assignment raise UnboundLocalError. Use a different name.

Linters will complain about undefined references for this, passthrough, _, args, kwargs, and verb_name. Add # pylint: disable=undefined-variable at the top of every verb file.

The context object

from moo.sdk import context brings the per-task context proxy into scope. Most non-trivial verbs need it. The two attributes you’ll reach for most:

  • context.player — the Object that originated the command. Stays anchored to the session initiator across nested verb calls. Use this for “who is acting” logic.

  • context.parser — the parsed command. See “Parser methods” below.

context.caller shifts as verbs invoke other verbs (it tracks the verb’s owner for permission checks); context.player does not. They are the same object at the start of a command.

For the full attribute list (writer, task_id, task_time, caller_stack), see The DjangoMOO Runtime.

Parser methods

When a verb is dispatched from a command, context.parser exposes methods to read the parsed arguments. The headline calls are get_dobj() / get_dobj_str() for the direct object and get_pobj(prep) / get_pobj_str(prep) for indirect objects, with has_* predicates for each.

Use get_*_str() when the argument is plain text (a message, a name to create). Use get_*() when you expect the argument to refer to an existing game object — and let the exception propagate if it doesn’t.

For the full method reference and exception behaviour, see Command Parser Reference.

Sending output to players

Three mechanisms exist:

Mechanism

Recipient

Notes

print(msg)

The player who ran the command

Buffered until the verb finishes. The standard way for command verbs to show results.

obj.tell(msg)

Any player Object

Goes through $player.tell, applying gag-list filtering and paranoia tracking. Immediate.

write(obj, msg)

Any player Object

Low-level connection write, bypasses all filtering. Wizard-owned verbs only.

print() is what most command verbs use. obj.tell() is for sending to players other than the initiator (or when player preferences should be respected). write() is rare — only for system notifications that must skip filtering.

return "string" does not display anything in a command verb. The return value goes back to whatever invoked the verb — discarded for top-level player commands. Always print() for player-visible output; use a bare return to exit early:

if not context.parser.has_dobj_str():
    print(f"Usage: {verb_name} <target>")
    return

print("Done.")

For tests where there is no live SSH connection, tell() and write() emit RuntimeWarning(f"ConnectionError({obj}): {msg}"). Capture with pytest.warns(RuntimeWarning) (see Writing Tests for Your Verbs).

Reading and writing properties

The Django ORM is available, but the helper methods on Object are shorter and walk the inheritance chain:

description = obj.get_property("description")
print(description)

obj.set_property("description", "A dark room.")

__getattr__ on Object lets you write obj.description directly, but it tries verb dispatch first and only falls through to property lookup on miss — so it’s two queries. Use get_property() when you know it’s a property.

Don’t pair has_property with get_property. That’s two database queries for the same data. Catch the absence with try/except:

from moo.sdk import NoSuchPropertyError

try:
    description = obj.get_property("description")
except NoSuchPropertyError:
    description = "You see nothing special."

Multi-line description text is automatically reflowed by the description verb in root_class/description.py via _.string_utils.rewrap(): single newlines collapse to spaces, double newlines become paragraph breaks, and each paragraph wraps to 80 columns. To get the same behaviour for a custom help verb or note’s read verb, call rewrap explicitly:

text = obj.get_property("body")
print(_.string_utils.rewrap(text))

obj.save() is only required after changing intrinsic fields like name, unique_name, obvious, or owner. set_property saves the property row directly.

Error handling

Every exception in moo.core.exceptions inherits from UserError. When a UserError propagates out of a verb, the task runner (moo.core.tasks.parse_command) catches it and shows the message to the player as a bold red line. No try/except boilerplate is needed just to report errors.

The common exceptions are all importable from moo.sdk:

exception moo.core.exceptions.UserError(message, data=None)

Superclass for any error that should be displayed to the player who triggered it. The task runner catches every UserError raised by a verb and renders it as a bold red line; verbs do not need to try/except around calls that may raise these. Subclasses customize the default message rendered to the player.

exception moo.core.exceptions.UsageError(message, data=None)

Raise when the player invoked a verb with bad syntax or missing arguments. The constructor takes the message string verbatim — that string is what the player sees. raise UsageError(f"Usage: {verb_name} <target>") is the conventional pattern.

exception moo.core.exceptions.NoSuchObjectError(name)

Raised when a name does not resolve to any object in scope — typically by Parser.get_dobj() or moo.sdk.lookup(). Default message: There is no '<name>' here.

exception moo.core.exceptions.NoSuchVerbError(name)

Raised by the parser when no verb on any candidate object matches the typed command. Default message: I don't know how to do that.

exception moo.core.exceptions.NoSuchPropertyError(name, origin=None)

Raised by Object.get_property() when the named property does not exist on the object or any of its ancestors. Default message: There is no '<name>' property defined.

exception moo.core.exceptions.AmbiguousObjectError(name, matches, message=None)

Raised when a name resolves to more than one object. Default message: When you say, "<name>", do you mean <obj1>, <obj2>, or <obj3>? — the matching objects are listed by name and #id so the player can disambiguate.

exception moo.core.exceptions.AmbiguousVerbError(name, matches)

Raised when verb dispatch finds more than one matching verb at the same object. Default message: More than one object defines "<name>": <obj1>, <obj2>, and <obj3>.

exception moo.core.exceptions.NoSuchPrepositionError(prep)

Raised by parser methods like Parser.get_pobj_str() when the requested preposition was not present in the player’s command. Default message: I don't understand you.

exception moo.core.exceptions.QuotaError(message, data=None)

Raised when @create is invoked by a player whose object quota is exhausted. Default message: You don't have enough quota to create that.

exception moo.core.exceptions.AccessError(accessor, access_str, subject)

Subclass of Python’s PermissionError raised by model-layer permission checks (Object.save(), Verb.__call__(), etc.) when the caller lacks the required permission. Default message: <accessor> is not allowed to '<action>' on <subject>. The task runner catches PermissionError automatically.

Letting get_dobj() raise NoSuchObjectError is the right pattern when the argument must resolve to a real object — the player sees the canned message automatically. Catch only when you want a different message or an alternative path:

from moo.sdk import NoSuchObjectError

try:
    target = context.parser.get_dobj()
except NoSuchObjectError:
    print("You'll need to be more specific.")
    return

UsageError is the conventional way to signal bad syntax:

from moo.sdk import UsageError

if not context.parser.has_dobj_str():
    raise UsageError(f"Usage: {verb_name} <target>")

Any uncaught exception that isn’t a UserError shows "An error occurred while executing the command." to regular players and a full traceback to wizards.

Permission checks

For verbs that mutate state, check before touching the database:

if not this.can_caller("write"):
    print("Permission denied.")
    return

can_caller(perm) consults the ACL on this against the current context.caller. Common permission names: read, write, execute, move, transmute, derive, develop, entrust, grant. See How Permissions Work in Verbs for the full set.

Validating arguments

Validate early; report once with a single print() and an early return:

if not context.parser.has_dobj_str():
    print(f"Usage: {verb_name} <target>")
    return

Or raise UsageError and let the task runner format it:

from moo.sdk import UsageError

if not context.parser.has_dobj_str():
    raise UsageError(f"Usage: {verb_name} <target>")

A real verb, end to end

default/verbs/thing/take.py puts most of the patterns above into one file:

#!moo verb take --on $thing --dspec this --ispec from:any

# pylint: disable=return-outside-function,undefined-variable

from moo.sdk import context, NoSuchObjectError

# If "from <target>" was given, verify the object is actually placed on/near that target.
if context.parser.has_pobj_str("from"):
    try:
        from_target = context.parser.get_pobj("from")
        placement = this.placement
        if placement is None or placement[1] != from_target:
            tname = context.parser.get_pobj_str("from")
            print(f"{this.title()} isn't on the {tname}.")
            return
    except NoSuchObjectError:
        tname = context.parser.get_pobj_str("from")
        print(f"There is no '{tname}' here.")
        return

title = this.title()
if this.location == context.player:
    print(f"You already have {title} in your inventory.")
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)
else:
    print(this.take_failed_msg(title))
    if msg := this.otake_failed_msg(title):
        this.location.announce(msg)

What’s going on:

  • The shebang fires this verb when take widget matches a $thing and optionally accepts a from <target> clause.

  • context.parser.has_pobj_str("from") and get_pobj("from") extract the optional from argument, treating “object not found” as a user-facing error.

  • this.title() is a helper verb on $thing that returns the formatted name.

  • this.moveto(context.player) calls another verb on the object — see Advanced Verb Patterns for how that works.

  • this.take_succeeded_msg(title) returns a pronoun-substituted string. Helper verbs like this do return values (because they’re invoked as methods, not as parser commands). The pattern is covered in Advanced Verb Patterns.

  • this.location.announce(msg) broadcasts to everyone else in the room.

Where to go next