Skip to content

Inline Scripting internals

Overview

Inline scripts on BPMN elements (bpmn:ScriptTask, camunda:ExecutionListener, camunda:TaskListener) are edited in real VS Code editor tabs instead of the properties-panel textarea. A custom FileSystemProvider registered for the bpmn-script:// URI scheme stores the script bodies in memory; edits are streamed back to the webview keystroke-by-keystroke and persisted through the bpmn-js command stack. A CompletionItemProvider scoped to the same scheme drives Camunda 7 IntelliSense (execution, task, eventName) per surface.

The webview surfaces the entry points: a context-pad button on script tasks and properties-panel buttons on listener rows. Both fire a single bpmn-js event that the webview translates into an OpenScriptEditorCommand.

System overview

ComponentRole
BpmnScriptFileSystemIn-memory FileSystemProvider for the bpmn-script:// scheme
ScriptTaskServiceLifecycle: open / track / sync / clean up virtual script documents
ScriptCompletionProviderPer-language IntelliSense scoped to bpmn-script:// URIs
scriptApi (domain)Bean and method definitions surfaced as completions
ScriptLanguage (domain)Camunda scriptFormat ↔ VS Code language id ↔ file extension
scriptEditorButtons (webview)Injects "Edit Script" buttons into listener properties-panel rows
scriptTaskContextPad (webview)Adds "Edit Script" entry to the script-task context pad

ScriptTaskService, BpmnScriptFileSystem, and ScriptCompletionProvider are constructed in apps/vscode-plugin/src/composition/scriptFeature.ts and registered before any editor controller resolves. ScriptTaskService.register() subscribes to workspace.onDidChangeTextDocument and window.tabGroups.onDidChangeTabs — those listeners are what propagate edits and clean up tracking state.

Entry points

  • Webview, script taskscriptTaskContextPad adds an "Edit Script" entry to the bpmn:ScriptTask context pad. Clicking it fires OPEN_SCRIPT_EDITOR_EVENT on the bpmn-js event bus.
  • Webview, listenerscriptEditorButtons injects a button into every listener row, plus one in the script-task Script group header. Listener buttons are always rendered regardless of listener implementation type; the click handler converts non-inline-script listeners (Java class / expression / delegate expression / external-resource script) into inline scripts before firing the event.
  • Webview bridgeapps/bpmn-webview/src/main.ts subscribes via BpmnModeler.onOpenScriptEditor() (which wraps the bpmn-js event bus) and forwards the event as OpenScriptEditorCommand over the webview message channel.
  • Host side — the BPMN WebviewMessageRouter dispatches OpenScriptEditorCommand to openScriptEditorHandler (bpmnMessageHandlers.ts), which calls ScriptTaskService.openScriptEditor(). A second handler on GetBpmnModelerSettingCommand (resyncScriptTasksHandler) calls ScriptTaskService.resyncOpenDocuments(editorId) on every webview reload.

Key files

FilePurpose
apps/vscode-plugin/src/composition/scriptFeature.tsRegisters BpmnScriptFileSystem for the bpmn-script scheme, constructs ScriptTaskService and ScriptCompletionProvider
apps/vscode-plugin/src/scriptTask/infrastructure/BpmnScriptFileSystem.tsFileSystemProvider impl — Map<string, Uint8Array> keyed by URI path; fires FileChangeEvents for create / change / delete
apps/vscode-plugin/src/scriptTask/controller/ScriptTaskService.tsOpen / track / sync / clean up virtual script documents; URI scheme; format prompt; resync after webview reload
apps/vscode-plugin/src/scriptTask/controller/ScriptCompletionProvider.tsCompletionItemProvider scoped to bpmn-script scheme; root + member completion modes
apps/vscode-plugin/src/scriptTask/domain/scriptCompletion.tsPure helpers — parseKindFromUri, matchMemberAccess (testable without vscode)
apps/vscode-plugin/src/scriptTask/domain/scriptApi.tsCamunda 7 bean and method catalogue (DELEGATE_EXECUTION_METHODS, DELEGATE_TASK_METHODS, beansFor)
apps/vscode-plugin/src/scriptTask/domain/scriptLanguage.tsScriptLanguage value object — supported formats, extensions, language ids
apps/vscode-plugin/src/scriptTask/domain/ScriptUri.tsScriptUri value object — encodes the bpmn-script:/… URI shape (slug, filename, editor hash)
apps/vscode-plugin/src/modeler/bpmn/controller/webview-handlers/bpmnMessageHandlers.tsopenScriptEditorHandler + resyncScriptTasksHandler, dispatched by the BPMN WebviewMessageRouter
apps/vscode-plugin/src/modeler/bpmn/controller/editor-participants/ScriptTaskTeardownParticipant.tsCalls disposeForEditor when the BPMN editor closes
libs/shared/src/lib/modeler.tsOpenScriptEditorCommand, UpdateScriptContentQuery, UpdateScriptFormatQuery, ScriptKind
apps/bpmn-webview/src/main.tsBridges OPEN_SCRIPT_EDITOR_EVENT (bus) ↔ OpenScriptEditorCommand (host) and applies UpdateScriptContentQuery / UpdateScriptFormatQuery to the model
apps/bpmn-webview/src/app/scriptEditorButtons.tsListener-row "Edit Script" buttons in the properties panel
apps/bpmn-webview/src/app/scriptTaskContextPad.ts"Edit Script" entry in the script-task context pad

URI scheme

Every open script lives at:

bpmn-script:/<editorHash>/<elementId>/<slug>/<filename>
SegmentSourcePurpose
<editorHash>ScriptUri.hashEditorId(editorId) — short hash of the BPMN document URIIsolates scripts per diagram so two diagrams with overlapping element IDs do not collide
<elementId>The hosting BPMN element's id (script task, or listener's parent)Per-element namespace
<slug>ScriptUri.slugscript-task, execution-listener-<idx>[-<event>], task-listener-<idx>[-<event>]Distinguishes multiple scripts on the same element; consumed by parseKindFromUri to scope completions
<filename>ScriptUri.filename — sanitized element id plus a short discriminatorHuman-readable tab label

Filename examples:

SurfaceTab label
bpmn:ScriptTask Task_1Task_1.js
Execution listener start (idx 0) on Task_1Task_1.execution-start.js
Second start execution listener on Task_1Task_1.execution-start-1.js
Task listener create on UserTask_1UserTask_1.task-create.js

The <elementId> path segment is the raw id; the filename is sanitized with [^A-Za-z0-9_-]_ because XML NCNames permit characters that aren't clean cross-platform filenames (e.g. dots and colons).

Message protocol

MessageDirectionPurpose
OpenScriptEditorCommandwebview → hostOpen inline script for (elementId, kind, listenerIndex, eventName) with current scriptFormat and content
UpdateScriptContentQueryhost → webviewPush edited content back to the modeler so it can write the moddle property and persist via the command stack
UpdateScriptFormatQueryhost → webviewPersist a Quick-Pick'ed scriptFormat back to the model so subsequent opens skip the prompt

(elementId, kind, listenerIndex) is the addressing tuple for the moddle property on both directions. eventName flows host-bound only — it's used to build the editor tab title and the URI slug, not to identify the target.

Interaction flow

Opening a script

Live edit propagation

Hidden-webview replay

When the BPMN tab is not visible, editorStore.postMessage throws "The active editor is hidden." — the user can still be typing in the script tab. ScriptTaskService defends against silent edit loss:

Completion provider

ScriptCompletionProvider registers for the bpmn-script scheme on every supported language (javascript, groovy, python, ruby). It runs in two modes:

ModeTriggerReturns
Member completionTrailing . after a known beanBean's methods rendered as snippets with parameter placeholders
Root completionWord being typed at root scopeBean names available in the current kind

Beans-in-scope are derived from the URI slug via parseKindFromUri:

ScriptKindBeans
script-taskexecution
execution-listenerexecution, eventName
task-listenerexecution, task, eventName

scriptApi.ts is the single source of truth for method names, parameter types, return types, and human-readable descriptions.

Cleanup

TriggerPathBehaviour
User closes script tabonTabsChangedcleanupClosedScriptRemoves from openDocuments; deletes the slug folder from scriptFs so a re-open picks up fresh content
Tab moves between groupsSame pathNo-op — isUriOpenInAnyTab detects the move (close + open pair)
Tab close while BPMN webview hiddenCleanup is deferredpendingResync carries the unwritten edit — performCleanup runs only after resyncOpenDocuments has replayed
BPMN editor disposeddisposeForEditorTracking state cleared synchronously, then orphaned tabs closed, then scriptFs.deleteByPrefix(/<editorHash>/)

Gotchas

  • Edits propagate per keystroke, not on save. Ctrl+S on a virtual script file is effectively a no-op — BpmnScriptFileSystem is in-memory. The BPMN file becomes dirty as soon as the moddle update lands; that's the persistence point.
  • writingGuard is mandatory in onVirtualDocumentChanged. The service itself writes to scriptFs to keep the in-memory bytes in sync with the editor buffer; without the guard the change event re-enters the handler.
  • Use onTabsChanged, not onDidCloseTextDocument. VS Code keeps the TextDocument alive for a short window after the tab closes (cheap re-opens). If we keyed cleanup off document close, a quick close-reopen with a different language would resurface the cached doc with stale content.
  • Cleanup must be deferred when the webview is hidden. The only copy of the unwritten edit lives in scriptFs. Cleaning up before the resync runs drops the buffered keystrokes silently. pendingResync.has(editorId) guards performCleanup.
  • Slug folder is the source of truth for ScriptKind. parseKindFromUri reads segments[segments.length - 2] — the slug folder, never the filename. The filename is purely cosmetic for the tab strip.
  • Element-id sanitization is filename-only. The <elementId> URI path segment carries the raw id. Sanitizing the path too would break the disposeForEditor prefix delete.
  • tsserver cannot see sibling files in a FileSystemProvider-backed scheme. That's why JavaScript completions go through the same CompletionItemProvider as the JSR-223 languages instead of a camunda.d.ts next to the script — the inferred TS project would never load it.