nexu-io/open-design · error · Error
pluginWorkflowId requires a validated externalPluginContext
Error message
pluginWorkflowId requires a validated externalPluginContext
What it means
Thrown by the brief collector's collect() when the caller passes `pluginWorkflowId` without also passing `externalPluginContext`. The collector treats plugin correlation as a paired contract: a workflow id is only meaningful when attributed to a validated external plugin, so providing one without the other is rejected. The check uses hasOwnProperty('externalPluginContext') so explicitly passing undefined as the context still counts as 'provided' and goes through validateExternalPluginContext instead.
Source
Thrown at apps/daemon/src/mcp-brief.ts:376
const localeSource =
typeof input.locale === 'string' && input.locale.trim().length > 0
? 'provided'
: 'fallback';
const locale = readBriefLocale(input.locale);
const knownAnswers = readAnswerRecord(input.knownAnswers, 'knownAnswers');
const skip = input.skip === true;
const hasPluginContext = Object.prototype.hasOwnProperty.call(
input,
'externalPluginContext',
);
const externalPluginContext = hasPluginContext
? validateExternalPluginContext(input.externalPluginContext)
: undefined;
const pluginWorkflowId = hasPluginContext
? validatePluginWorkflowId(input.pluginWorkflowId)
: undefined;
if (!hasPluginContext && input.pluginWorkflowId !== undefined) {
throw new Error(
'pluginWorkflowId requires a validated externalPluginContext',
);
}
const decision = collectOpenDesignBrief({
artifactType,
knownAnswers,
skip,
});
const briefDraftId = randomUUID();
const nonce = randomBytes(24).toString('hex');
const expiresAt = at + ttlMs;
drafts.set(briefDraftId, {
artifactType,
projectTitle,
locale,
localeSource,
knownAnswers: { ...decision.answers },
nonce,View on GitHub (pinned to 5be4028344)
Solutions
- Pass both fields together: include a fully-validated externalPluginContext whenever you pass pluginWorkflowId.
- If you have no plugin context, omit pluginWorkflowId entirely (do not pass it as undefined alongside a missing context).
- Build the externalPluginContext via validateExternalPluginContext() before sending so it cannot be dropped by a validation gap.
- Audit request payloads at the boundary to enforce the pairing (the two fields are either both present or both absent).
Example fix
// before
collect({ artifactType: 'deck', pluginWorkflowId: '01HZX...' });
// after — pair the workflow id with its plugin context
collect({
artifactType: 'deck',
externalPluginContext: {
id: OPEN_DESIGN_PLUGIN_ID,
version: '1.0.0',
distributionMechanism: 'official',
publisherClass: 'first-party',
},
pluginWorkflowId: '01HZX...',
}); Defensive patterns
Strategy: type-guard
Validate before calling
function hasPluginContextPair(input: unknown): boolean {
if (!input || typeof input !== 'object') return true;
const hasContext = Object.prototype.hasOwnProperty.call(input, 'externalPluginContext');
const hasWorkflow = Object.prototype.hasOwnProperty.call(input, 'pluginWorkflowId');
// Either both present or both absent.
return hasContext === hasWorkflow;
}
if (!hasPluginContextPair(input)) {
throw new Error('pluginWorkflowId and externalPluginContext must be supplied together.');
} Type guard
import { validateExternalPluginContext, validatePluginWorkflowId } from './mcp-observability';
type PluginCorrelation = { externalPluginContext: ExternalPluginContext; pluginWorkflowId: string };
function resolvePluginCorrelation(input: Record<string, unknown>): PluginCorrelation | undefined {
const hasContext = Object.prototype.hasOwnProperty.call(input, 'externalPluginContext');
const hasWorkflow = Object.prototype.hasOwnProperty.call(input, 'pluginWorkflowId');
if (!hasContext && !hasWorkflow) return undefined;
if (hasContext !== hasWorkflow) {
throw new Error('pluginWorkflowId requires externalPluginContext (and vice versa).');
}
return {
externalPluginContext: validateExternalPluginContext(input.externalPluginContext),
pluginWorkflowId: validatePluginWorkflowId(input.pluginWorkflowId),
};
} Prevention
- Always pass pluginWorkflowId and externalPluginContext as a pair, or omit both.
- Build externalPluginContext via validateExternalPluginContext at the boundary.
- Audit request payloads to enforce the pairing before reaching the collector.
When it happens
Trigger: Calling the brief collect API with `{ pluginWorkflowId: '<uuid>' }` but no `externalPluginContext` field. Or copying fields from a partial request that included the workflow id but dropped the plugin context object.
Common situations: External plugin omits the context object but sends its workflow id for analytics; a shim/adapter forwards pluginWorkflowId unconditionally; refactoring removes externalPluginContext but leaves pluginWorkflowId; client assumes pluginWorkflowId is independently optional.
Related errors
- The Open Design brief is incomplete. Missing: ${missing.join
- artifactType must be one of: ${Object.keys(openDesignBriefCa
- projectTitle must be at most 256 characters
- ${field} must be a non-empty string
- ${field} must be an object
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/b309bdcf49ee5223.
Report an issue: GitHub.