XBlock Asides Reference#

Tags: developer reference

This reference describes the public API surface for XBlock Asides as defined in the xblock package. It covers the XBlockAside base class, the decorators and classmethods that subclasses use, the runtime hooks that govern Aside discovery and rendering, the entry point group that registers asides as plugins, and the OLX serialization contract.

For an introduction to what asides are and when to use them, see About XBlock Asides. For a guided walkthrough, see Add an XBlock Aside or Quickstart: Build Your First XBlock Aside.

The XBlockAside Class#

The base class for all asides is xblock.core.XBlockAside. It inherits from Plugin and Blocklike, so an Aside can declare scoped fields, mark methods as handlers, and be loaded as a plugin in the same way as an XBlock.

from xblock.core import XBlockAside

class MyAside(XBlockAside):
    """An Aside that decorates one or more XBlock views."""

Class Attributes#

entry_point

The Python entry point group used to discover Aside plugins. Set on the base class to "xblock_asides.v1". Subclasses should not change this.

fields

The set of fields declared on the Aside, automatically collected from class-level Field declarations. Inherited from Blocklike.

Decorators#

XBlockAside.aside_for(view_name)#

A classmethod decorator that marks a method as the Aside view for the named XBlock view. When the runtime renders an XBlock view called view_name, every applicable Aside whose class declares a method decorated with @XBlockAside.aside_for(view_name) is invoked, and the returned fragments are appended to the host block’s rendered fragment.

Signature of the decorated method

@XBlockAside.aside_for("student_view")
def student_view_aside(self, block, context=None):
    ...
    return Fragment(...)

The decorated method takes:

  • self — the Aside instance.

  • block — the host XBlock instance being rendered.

  • context — an optional dictionary of context data passed by the caller of render.

The method must return a Fragment.

Multiple views per Aside

A single method may decorate multiple views by stacking decorators, and a single Aside class may define different methods for different views.

class MyAside(XBlockAside):

    @XBlockAside.aside_for("student_view")
    def student_view_aside(self, block, context=None):
        return Fragment("<div>Student-side Aside</div>")

    @XBlockAside.aside_for("author_view")
    @XBlockAside.aside_for("studio_view")
    def studio_aside(self, block, context=None):
        return Fragment("<div>Author/Studio Aside</div>")

Common view names

The view names commonly decorated by asides include:

View name

When the runtime renders it

student_view

The learner-facing view of a block in the LMS.

author_view

The author-facing preview of a block in Studio.

studio_view

The author-facing edit form of a block in Studio.

XBlocks may define additional views; an Aside can decorate any of them by name.

Classmethods#

XBlockAside.should_apply_to_block(cls, block)#

A classmethod that returns True if the Aside should apply to the given block, and False otherwise. The default implementation returns True unconditionally.

Override this method to restrict the Aside to specific block types, courses, or feature flags. The runtime calls this method on every Aside-block pair before rendering, and asides for which it returns False are skipped.

@classmethod
def should_apply_to_block(cls, block):
    block_type = getattr(block, "category", None)
    return block_type in {"problem", "video"}

The block argument is the host XBlock instance. Most filtering logic inspects block.scope_ids.block_type, block.category, the course context derived from block.scope_ids.usage_id, or platform feature flags.

Instance Methods#

aside_view_declaration(view_name)#

Return the bound method on this Aside instance that is decorated with @XBlockAside.aside_for(view_name), or None if no such method exists. The runtime uses this method to look up the Aside’s view function when rendering. Subclasses do not normally need to call it directly.

needs_serialization()#

Return True if the Aside has any field whose value differs from its default. The default implementation iterates over the Aside’s fields and returns True if any field is set on this instance. The runtime calls this method during OLX export to decide whether to serialize the Aside as an XML element. Subclasses rarely need to override this.

parse_xml(node, runtime, keys) and add_xml_to_node(node)#

Inherited from Blocklike. Override these to customize how the Aside is read from and written to OLX. The default implementations serialize the Aside’s fields as XML attributes and child elements, identical to the default XBlock behavior.

Fields and Scopes#

An Aside declares fields the same way an XBlock does, with class-level field declarations. Fields are scoped, and the scope determines where the field’s value is stored and which entities share it.

from xblock.core import XBlockAside
from xblock.fields import Boolean, Scope, String

class MyAside(XBlockAside):
    enabled = Boolean(
        display_name="Enabled",
        default=False,
        scope=Scope.settings,
        help="Whether this Aside is active for this block.",
    )

    last_message = String(
        default="",
        scope=Scope.user_state,
        help="The most recent message for this user-block pair.",
    )

Before saving a field like last_message from a handler, see the valid-scopes caveat below — saving a Scope.user_state field while an Aside runs under Studio raises an error.

The supported scopes are the standard XBlock scopes from xblock.fields: Scope.content, Scope.settings, Scope.user_state, Scope.user_state_summary, Scope.preferences, and Scope.user_info. An Aside’s field values are conceptually stored under the Aside’s own usage ID, separate from the host block’s field values — but do not assume this means two different Aside classes can safely share a field name, even though the abstract xblock package’s own key-construction logic embeds the Aside’s entry-point type into that usage ID.

On the platform’s Split modulestore — the default and current modulestore — same-named ``Scope.content``/``Scope.settings`` fields on different Asides collide, confirmed in practice. The KVS the platform actually uses to store course-structure data (SplitMongoKVS, in xmodule/modulestore/split_mongo/split_mongo_kvs.py) buckets Aside field storage by the host block’s type (key.block_scope_id.block_type), not the Aside’s own type — every Aside attached to a given block type shares one dictionary of field name to value for that block. The read path (xmodule/modulestore/split_mongo/runtime.py) reinforces this: it pre-merges every attached Aside’s persisted fields into that single per-block-type dictionary before any individual Aside instance is even constructed. Two Aside classes attached to the same block type that both declare a field named, say, enabled, read and write the same stored value — checking one Aside’s checkbox in Studio’s author view can visibly and functionally check the other Aside’s checkbox too, even when the two Asides use entirely different JavaScript and DOM selectors, because the underlying field value they’re both bound to really is the same one. Last-write-wins on export, too. This has been directly reproduced with ol-openedx-chat and rapid-response-xblock installed together, sharing a field name.

Choose a field name that no other Aside on your deployment is likely to use — the confirmed collision above makes this a real data-safety requirement, not just a debugging convenience. This is separate from, and adds to, the render- and JavaScript-layer interference described in the About XBlock Asides concept doc’s “Multiple Asides on a Single Block Are Not Reliable” limitation.

Only ``Scope.content`` and ``Scope.settings`` can be saved while an Aside is running under Studio, confirmed in practice. xmodule/modulestore/split_mongo/split_mongo_kvs.py:24 defines SplitMongoKVS.VALID_SCOPES = (Scope.parent, Scope.children, Scope.settings, Scope.content) — the two internal structural scopes, plus the two course-structure scopes discussed above. Every other field scope (Scope.user_state, Scope.user_state_summary, Scope.preferences, Scope.user_info) is absent from that list. Calling a handler that persists a field in one of those scopes (any code path that reaches block.save() → force_save_fields → _field_data.set_many → the KVS’s set/set_many) raises xblock.exceptions.InvalidScopeError from that same set() method, because Studio’s preview and author-view rendering is backed by SplitMongoKVS. This is distinct from the field-collision problem above: it isn’t that two Asides’ values might collide, it’s that persisting a field in any of these four excluded scopes raises an error in Studio at all, regardless of naming. If your Aside needs one of these scopes for the learner-facing view, expect it to work only through the LMS’s runtime (not backed by SplitMongoKVS), and never invoke a save of such a field from a Studio-side handler or author view.

Handlers#

An Aside can define AJAX handlers using the standard @XBlock.handler decorator from xblock.core. The handler URL is generated by the runtime in the same way as for an XBlock, so an Aside’s view fragment can call its own handler with self.runtime.handler_url(self, "handler_name").

from xblock.core import XBlock, XBlockAside

class MyAside(XBlockAside):

    @XBlock.handler
    def submit_feedback(self, request, suffix=""):
        ...
        return Response(json_body={"ok": True})

The handler signature, request object, and response handling are identical to XBlock handlers.

Entry Point Registration#

An Aside is discovered by the runtime through a Python entry point in the xblock_asides.v1 group. Declare the entry point in the package’s pyproject.toml:

[project.entry-points."xblock_asides.v1"]
my_aside = "my_package.Aside:MyAside"

Or in setup.py:

setup(
    entry_points={
        "xblock_asides.v1": [
            "my_aside = my_package.Aside:MyAside",
        ],
    },
)

The entry point name on the left of the equals sign is the Aside’s type name. It is used as the XML tag when the Aside is serialized to OLX, and as the key in ScopeIds when an Aside instance is constructed. Choose a name that is unique across all installed asides on a deployment.

Entry points are loaded through XBlockAside.load_classes(), which defaults to fail_silently=True: if an Aside’s module raises an exception on import, the loader logs a warning and simply omits that Aside from the registered list, rather than raising. A broken Aside package can therefore fail to load with no error visible anywhere except the log — see Add an XBlock Aside for how this shows up in practice when troubleshooting a missing Aside.

Runtime API#

The methods listed below are defined on Runtime and govern Aside discovery, instantiation, and rendering. Aside subclasses do not normally call these directly. They are documented here so runtime implementors and Aside authors can understand the lifecycle.

Discovery#

runtime.applicable_aside_types(block)

Return the list of Aside type names that may apply to block. The default implementation returns every Aside class registered through the xblock_asides.v1 entry point. A runtime may override this to filter by user, course, or other context.

The edx-platform LMS runtime overrides this through lms_applicable_aside_types in lms/djangoapps/lms_xblock/runtime.py, which gates Aside rendering on the XBlockAsidesConfig Django configuration model. When the model’s current revision has enabled=False, no asides render in the LMS. When enabled, asides do not render on block types listed in the model’s disabled_blocks field (default value: "about course_info static_tab").

Studio has its own, separate gate. The CMS overrides Aside discovery independently, through preview_applicable_aside_types in cms/djangoapps/contentstore/views/preview.py, which consults StudioConfig.asides_enabled(block_type) — a second ConfigurationModel, defined in cms/djangoapps/xblock_config/models.py, with the same shape as XBlockAsidesConfig (an enabled flag plus a disabled_blocks field, same default: "about course_info static_tab"). This gate applies to every preview view Studio renders for a block — student_view, public_view, and author_view alike — not just one of them.

XBlockAsidesConfig and StudioConfig are two independent ConfigurationModel rows. Like every ConfigurationModel, each one defaults to enabled=False until an operator explicitly saves an enabled revision, so out of the box no Aside renders in either the LMS or Studio, regardless of installation or registration. Enabling one does not enable the other — an operator who wants an Aside to render in both the LMS and Studio must enable both models. See Add an XBlock Aside for the administrative steps.

Asides in the Authoring MFE. The Authoring micro-frontend has no native code for rendering or toggling Asides. Its unit editor page embeds the Studio unit page in an iframe (container_embed_handler in cms/djangoapps/contentstore/views/component.py), so an Aside’s author_view fragment — gated by StudioConfig exactly as described above — appears inside that iframe when authors edit a unit. There is no separate MFE-specific toggle. The embedded page and the MFE are not otherwise isolated: they already exchange a real postMessage today (a saveEditedXBlockData message the embedded page sends after a save, which the MFE listens for — see Add an XBlock Aside for where this shows up in an Aside’s own JavaScript).

runtime.load_aside_type(aside_type)

Return the XBlockAside subclass corresponding to the given aside_type string. Raises if no Aside is registered under that name.

Instantiation#

runtime.create_aside(block_type, keys)

Construct an Aside instance of the named block_type (the Aside type name, despite the parameter name) with the given ScopeIds. Returns an XBlockAside instance.

runtime.get_aside_of_type(block, aside_type)

Construct an Aside of the named type that is bound to the given host block. Generates the Aside’s definition and usage IDs from the host block’s IDs using the runtime’s id_generator. Returns an XBlockAside instance.

runtime.get_aside(aside_usage_id)

Construct an Aside instance from a previously known Aside usage ID. Used during OLX import and other paths where the Aside’s identity is already established.

runtime.get_asides(block)

Return the list of Aside instances that should decorate the given block. This method composes applicable_aside_types and each Aside’s should_apply_to_block filter, returning only the asides for which both pass.

Rendering#

runtime.render_asides(block, view_name, frag, context)

Called by the runtime’s render method after the block’s own view has produced its fragment. Iterates over get_asides(block), looks up each Aside’s view declaration for view_name, and dispatches layout to layout_asides. Returns the augmented Fragment.

runtime.layout_asides(block, context, frag, view_name, aside_frag_fns)

Execute the Aside view functions and combine their fragments with the block’s fragment. The default implementation appends each Aside’s wrapped fragment after the block’s fragment. Override this to control the placement, ordering, or conditional inclusion of asides. Any override must call wrap_aside around each Aside fragment to preserve client-side identification.

runtime.wrap_aside(block, Aside, view, frag, context)

Wrap an Aside’s fragment with a <div> carrying the Aside’s usage ID, the host block’s usage ID, and any JavaScript initialization metadata. Override this if you need a different wrapping element or different data- attributes.

Talking to the Learning MFE via postMessage#

An Aside’s fragment renders inside the same iframe as its host XBlock whenever the Learning micro-frontend displays that block, so an Aside’s own JavaScript can use the browser’s postMessage API to reach the parent MFE page. There are three layers to this, from “already works” to “build your own”:

Built-in message types. frontend-app-learning listens for a small set of message types on the iframe’s parent window, split across two hooks: useIFrameBehavior.ts (plugin.resize, plugin.videoFullScreen, plugin.autoAdvance, and a bare { offset } scroll message) and useModalIFrameData.js (plugin.modal / plugin.modal-close). Both listeners branch only on event.data.type and never check the sender, so any Aside can trigger them with no MFE-side changes:

window.parent.postMessage(
    {type: 'plugin.modal', payload: {open: true}},
    learningMfeBaseUrl
);

Avoid reusing plugin.resize for this purpose — the host page already posts it from its own document-size observer on every DOM mutation, so an Aside posting the same type competes with that loop instead of adding a new capability. plugin.modal has no such collision.

Custom message type plus your own listener — the general recipe. The four built-in types above are the only ones frontend-app-learning recognizes out of the box. For anything else — a custom drawer, a bespoke widget — an Aside can still post a message with any type it likes, but something has to be listening for it. That “something” is a deployment-time customization, not a core MFE feature: the Learning MFE loads an env.config.jsx file at startup (the standard Open edX MFE runtime-configuration mechanism), which can run arbitrary side-effect JavaScript in addition to ordinary config values — for example, dynamically importing and initializing a small script that does its own window.addEventListener('message', ...), checks event.origin against an allow-listed origin and event.data.type against the string(s) it cares about, and mounts whatever UI it wants in response. The recipe has two halves:

  1. The Aside’s JavaScript posts {type: 'your-namespace::your-event', payload: {...}} to a known target origin.

  2. The Learning MFE’s env.config.jsx (or equivalent build-time customization) registers a listener for that exact type and origin.

Neither half requires forking or patching frontend-app-learning itself.

A working example of the pattern. MIT Open Learning’s ol-openedx-chat Aside implements exactly this: its ai_chat.js posts {type: "smoot-design::tutor-drawer-open", ...} to a target origin sourced from settings.LEARNING_MICROFRONTEND_URL (passed into the fragment’s JavaScript through initialize_js(json_args=...), which is the safer, explicit convention — prefer it over relying on document.referrer). On the receiving side, MIT’s deployment injects smoot-design’s AiDrawerManager bundle through their own env.config.jsx, which listens for that exact message type (and its current name, smoot-design::ai-drawer-open) and mounts a chat drawer. This is cited as one concrete, running implementation of the pattern above, not as something every deployment needs to depend on — an operator can build an equivalent listener around any custom message type without using MIT’s packages at all.

OLX Serialization#

When a course is exported to OLX, the runtime serializes every Aside that returns True from needs_serialization() as an XML child element of its host block. The XML tag of the Aside element is the Aside’s entry point name. Field values are written as attributes or nested elements according to the Aside’s add_xml_to_node implementation.

On import, the runtime detects Aside elements by looking up their tag names in the registered xblock_asides.v1 entry points. If a tag does not resolve to a registered Aside, the import does not raise an error and does not report a failure — but it also does not simply drop the Aside element and keep the host block. The host block that carried the unresolvable Aside is dropped from the course entirely, with no error block, no placeholder, and nothing in the import result indicating anything went wrong. Field values for any successfully-matched Aside are read by its parse_xml implementation as usual.

Two practical consequences:

  • An Aside’s data only round-trips through OLX if both the source and destination platforms have the same Aside installed under the same entry point name. If they don’t, the cost isn’t limited to that Aside’s data — the host block goes missing too.

  • Some XBlocks do not preserve Aside child elements through their own export and import paths. See About XBlock Asides for the current list of known issues.

Render Pipeline Summary#

The full sequence of calls when a runtime renders an XBlock view is:

  1. runtime.render(block, view_name, context) is called.

  2. The runtime invokes the block’s view function and obtains a fragment.

  3. The runtime calls runtime.wrap_xblock on that fragment.

  4. The runtime calls runtime.render_asides(block, view_name, frag, context).

  5. render_asides calls runtime.get_asides(block), which uses applicable_aside_types(block) and each Aside’s should_apply_to_block(block) to compute the filtered list. An exception here is not caught by xblock either — confirmed in both Studio and the Learning MFE, it renders as an error block the same as an exception raised inside a view method (see the note on layout_asides below).

  6. For each surviving Aside, render_asides calls Aside.aside_view_declaration(view_name) to find the matching method.

  7. render_asides calls layout_asides, which invokes each Aside view function, calls wrap_aside on each result, and appends the wrapped fragments to the block’s fragment. Neither xblock nor layout_asides catches an exception raised by an Aside’s view function here — it propagates out of the render call. A platform catches it at a higher layer instead: an exception raised inside an Aside’s student_view renders as an error block in place of the host block in the Learning MFE, and an exception raised inside author_view renders as an error block on the unit page in Studio. Either way, a broken Aside visibly breaks its host block’s display rather than failing invisibly. (This is distinct from an Aside failing to load as a plugin, which is silent — see “Entry Point Registration” above.)

  8. The combined fragment is returned to the original caller.

See also

About XBlock Asides (concept)

Why asides exist, what problem they solve, and current limitations.

Add an XBlock Aside (how-to)

A step-by-step recipe for adding an Aside to existing XBlocks.

Quickstart: Build Your First XBlock Aside (quickstart)

A beginner-friendly walkthrough from zero to a running Aside.

Maintenance chart

Review Date

Working Group Reviewer

Release

Test situation