Skip to content

Reading Context and Performing Document Actions

Once your extension has called AppExtension.registerAppExtension() (see the overview), the host sends an app context into the extension and exposes a set of document actions you can call back into the host. Everything is imported from @contrail/extensions-sdk.

The SDK gives you a context helper plus app-specific action classes:

  • getAppContext() — the current context, regardless of which app launched the extension (board, plan, or showcase).
  • BoardsApp — read and mutate a board document.
  • PlanApp — read placeholders and add rows in a plan.
  • ShowcaseApp — read the current showcase, add and modify elements, add frames, and add items to the clipboard.

Synchronous reads vs. async calls

Reads that come straight from the context the host already sent are synchronous and return immediately: getCurrentBoard, getCurrentPlan, getAllPlanPlaceholders, getFilteredPlanPlaceholders, getCurrentShowcase.

Calls that go back to the host are async and return a Promise: getElements, recolorImages, addFramesFromTemplate, createNewContentAndAssignToEntity, addCanvasFrame.

Mutations are fire-and-forget and return void: addElements, modifyElements, deleteElements, addRows, clearSelectedRows, showMessage, addItemsToClipboard.

Each app class checks that the extension was launched in that app — for example BoardsApp throws "App extension has not been initialized with Board context." if you call it outside a board, and ShowcaseApp throws the equivalent for showcase.

Reading context (any app)

getAppContext() returns an AppContext. Use appContext.vibeIQApp to branch on which app you're in, and appContext.selectedElements for the elements the user had selected when launching a contextual extension. user is the current user.

import { getAppContext, VibeIQAppType } from '@contrail/extensions-sdk';

const context = getAppContext();
const app = context.appContext?.vibeIQApp; // 'boards' | 'plan' | 'showcase' | 'admin_console'
const user = context.user;

// Elements the user selected before launching (contextual extensions)
const selected = context.appContext?.selectedElements ?? [];
const firstItemRef = selected[0]?.modelBindings?.item; // reference to the bound item, if any

switch (app) {
  case VibeIQAppType.BOARDS:    /* use BoardsApp */ break;
  case VibeIQAppType.PLAN:      /* use PlanApp */ break;
  case VibeIQAppType.SHOWCASE:  /* use ShowcaseApp */ break;
}

See Contextual Extensions for more on selectedElements and modelBindings.

Board (BoardsApp)

A board is a canvas document made of elements (images, text, frames, etc.). Mutations are applied through the board's document service, so they participate in the board's normal undo/redo history.

Read board context and elements

The board context already includes the full document, so its elements are available directly on getCurrentBoard() — no call back to the host is needed to read them:

import { getAppContext, BoardsApp } from '@contrail/extensions-sdk';

// Board metadata + the full document (id, name, backingAssortmentId, document, ...)
const board = BoardsApp.getCurrentBoard();
console.log(board.id, board.name, board.backingAssortmentId);

// Elements come with the context — read and filter them directly.
const elements = board.document?.elements ?? [];
const imageElements = elements.filter((element) => element.type === 'image');

// The elements the user had selected when launching are also on the context.
const selectedElements = getAppContext().appContext?.selectedElements ?? [];

When to use getElements() instead

board.document.elements is a snapshot taken when the extension launched — it does not update as the board changes. Use BoardsApp.getElements(criteria) when you need the live document (for example after your extension has added or modified elements), or to scope to a single frame. Pass {} for the whole document or { frameId } for one frame:

const liveElements = await BoardsApp.getElements({});                          // current whole document
const frameElements = await BoardsApp.getElements({ frameId: 'frame-123' });   // one frame

Add, modify, and delete elements

Build elements with DocumentElementFactory from @contrail/documents so they get a generated id and a valid shape. The factory has type-specific helpers (createTextElement, createImageElement, createFrameElement) and a generic createElement(type, options).

import { BoardsApp } from '@contrail/extensions-sdk';
import { DocumentElementFactory } from '@contrail/documents';

// Add a text label to the canvas.
const label = DocumentElementFactory.createTextElement('Generated by my extension', {
  position: { x: 100, y: 100 },
  size: { width: 240, height: 60 },
});
BoardsApp.addElements([label]);

// Modify elements: pass the element id and the properties to change
BoardsApp.modifyElements([
  { id: label.id, changes: { position: { x: 320, y: 100 } } },
]);

// Delete elements (each must have an `id`)
BoardsApp.deleteElements([label]);

Recolor images, add frames, assign content

// Recolor image elements to a single hex code (or pass a hexMap for per-color mapping)
const recolored = await BoardsApp.recolorImages(imageElements, '#1d4ed8');

// Add frames from a saved frame template (by id or an inline template)
await BoardsApp.addFramesFromTemplate([
  { frameTemplateId: 'template-123', frameOptions: { position: { x: 0, y: 0 }, name: 'Look 1' } },
]);

// Create new content from elements and attach it to an entity (e.g. an item) as a viewable
await BoardsApp.createNewContentAndAssignToEntity(assignmentOptions, entity, elements);

Plan (PlanApp)

A plan is a spreadsheet-style surface of plan placeholders (rows).

Read the plan and its placeholders

import { PlanApp } from '@contrail/extensions-sdk';

// Plan metadata (id, name, targetAssortmentId, workspaceId, hasFilter, ...)
const plan = PlanApp.getCurrentPlan();

// All rows, or only the rows currently visible under the user's filter
const allRows = PlanApp.getAllPlanPlaceholders();
const visibleRows = PlanApp.getFilteredPlanPlaceholders(); // respects the active filter

Add rows and message the user

import { PlanApp } from '@contrail/extensions-sdk';

// Add rows to the plan (collection elements)
PlanApp.addRows(newCollectionElements);

// Clear the user's current row selection
PlanApp.clearSelectedRows();

// Show a transient message in the plan UI
PlanApp.showMessage('Imported 12 rows from the forecast');

Showcase (ShowcaseApp)

A showcase presents items across frames (lineboard pages), each with its own canvas document. ShowcaseApp exposes a synchronous context read, element mutations, frame creation, and a clipboard action.

Requires @contrail/extensions-sdk 1.0.23 or later.

Method Signature Returns Kind Host command
getCurrentShowcase () ShowcaseContext sync read
addElements (elements: ShowcaseDocumentElement[], frameId?: string) void mutation showcase:add_elements
modifyElements (changeObjects: ShowcaseDocumentElementChanges[], frameId?: string) void mutation showcase:modify_elements
addCanvasFrame (elements: ShowcaseDocumentElement[], frameOptions?: { name?: string; size?: { width: number; height: number } }) Promise<Frame> async showcase:add_canvas_frame
navigateToFrame ({ frameId: string } | { frameIndex: number }) Promise<{ frameId: string; frameIndex: number }> async showcase:navigate_to_frame
addItemsToClipboard (clipboardItems: { itemId: string; projectItemId?: string | null }[]) void mutation showcase:add_items_to_clipboard

Read the showcase context

import { ShowcaseApp, getAppContext } from '@contrail/extensions-sdk';

// Showcase metadata (id, name, currentFrame, frames, backingAssortmentId, ...)
const showcase = ShowcaseApp.getCurrentShowcase();
const currentFrame = showcase?.currentFrame;
const allFrames = showcase?.frames ?? [];

// Elements the user selected when launching (contextual extensions)
const selected = getAppContext().appContext?.selectedElements ?? [];

getCurrentShowcase() returns the same object as getAppContext().appContext?.showcase, but additionally validates that the extension was launched in a showcase. Like the board context, it is a snapshot taken when the extension launched — frames your extension adds afterwards won't appear in it.

Targeting a frame with frameId

addElements and modifyElements take an optional frameId:

  • Omitted — the command applies to the currently active frame.
  • Given — the command applies to the frame matching frameId (from showcase.frames), and the host switches the view to that frame as it applies the change.
  • Unknown — the host logs Frame not found for ID: … to the console and ignores the command; nothing throws in the extension.

Add and modify elements

Build elements with DocumentElementFactory from @contrail/documents, just like on a board. Mutations are applied through the showcase's document service, so they participate in the normal undo/redo history.

import { ShowcaseApp } from '@contrail/extensions-sdk';
import { DocumentElementFactory } from '@contrail/documents';

// Add a text label to the active frame
const label = DocumentElementFactory.createTextElement('Generated by my extension', {
  position: { x: 100, y: 100 },
  size: { width: 240, height: 60 },
});
ShowcaseApp.addElements([label]);

// Add to a specific frame instead
ShowcaseApp.addElements([label], 'frame-123');

// Modify elements: pass the element id and the properties to change
ShowcaseApp.modifyElements([
  { id: label.id, changes: { position: { x: 320, y: 100 }, style: { color: '#e11d48' } } },
]);

Add a new frame

addCanvasFrame creates a new frame containing the given elements and resolves with the created frame. frameOptions.size defaults to 1200 × 675; frameOptions.name defaults to an empty name.

import { ShowcaseApp } from '@contrail/extensions-sdk';
import { DocumentElementFactory } from '@contrail/documents';

const title = DocumentElementFactory.createTextElement('Spring drop', {
  position: { x: 80, y: 80 },
});

const frame = await ShowcaseApp.addCanvasFrame([title], {
  name: 'Look 1',
  size: { width: 1200, height: 675 },
});
console.log(frame.id, frame.name);

If the host fails to create the frame, the returned promise rejects.

navigateToFrame changes the frame the user is currently looking at, and resolves with the frame that ended up active. Target it either by frameId or by frameIndex — a 0-based position within showcase.frames. Pass exactly one of the two.

// By id
const byId = await ShowcaseApp.navigateToFrame({ frameId: 'frame-123' });
console.log(byId.frameId, byId.frameIndex); // 'frame-123', 4

// By 0-based index — jump to the first frame
const byIndex = await ShowcaseApp.navigateToFrame({ frameIndex: 0 });
console.log(byIndex.frameId, byIndex.frameIndex); // 'frame-abc', 0

The resolved frameIndex is useful after addCanvasFrame, since the frame list in getCurrentShowcase() is a launch-time snapshot and won't contain frames your extension added.

Unlike addElements and modifyElements — which log a warning and ignore an unknown frameId — the returned promise rejects if the target can't be resolved: an unknown frameId, a frameIndex that is negative or past the end of the frame list, or neither field supplied.

Add items to the clipboard

// Bulk-add items to the user's clipboard. Each entry needs an itemId; projectItemId is optional.
ShowcaseApp.addItemsToClipboard([
  { itemId: 'item-1' },
  { itemId: 'item-2', projectItemId: 'project-item-2' },
]);

Working with entities and types

Beyond document actions, extensions can read and write platform data through the host's session using EntitiesClient and TypesClient (also from @contrail/extensions-sdk). These proxy @contrail/sdk calls through the host, so they respect the current user's org and permissions.

import { EntitiesClient } from '@contrail/extensions-sdk';

const entities = new EntitiesClient();
const item = await entities.get({ entityName: 'item', id: 'item-1' });
await entities.update({ entityName: 'item', id: 'item-1', object: { name: 'Updated name' } });

For the connection/handshake and display configuration, see the extension overview; for selection-driven extensions, see Contextual Extensions.