siyuan-note/siyuan · error
Plugin ${pluginLabel} returned an invalid action
Error message
Plugin ${pluginLabel} returned an invalid action What it means
During the before-upload-assets pipeline, processPlugin waits for a plugin's respondWith decision and only accepts action 'replace' or 'cancel'. If the resolved decision has any other (or missing) action, it throws Error("Plugin ${pluginLabel} returned an invalid action"), aborting the upload chain for that plugin. This enforces the plugin API contract that a claimant must either replace or cancel the asset upload.
Source
Thrown at app/src/protyle/upload/pluginEvent.ts:439
discardResponse();
return fail(new Error(`Plugin ${pluginLabel} must use respondWith to replace or cancel an asset upload`));
}
if (responseError) {
discardResponse();
return fail(responseError);
}
if (emitResult.hasAsyncListener && !responseClaimed) {
return fail(new Error(`Plugin ${pluginLabel} must call respondWith synchronously before awaiting`));
}
if (!response) {
return processPlugin(index + 1);
}
return waitForDecision(response, task, pluginLabel, timeout).then(decision => {
if (decision?.action === "cancel") {
return cancel();
}
if (decision?.action !== "replace") {
throw new Error(`Plugin ${pluginLabel} returned an invalid action`);
}
const validationError = validateAssetUploadInput(decision.input);
if (validationError) {
throw new Error(`Plugin ${pluginLabel} returned invalid input: ${validationError}`);
}
const targetValidationError = validateTargetInput(decision.input, context);
if (targetValidationError) {
throw new Error(`Plugin ${pluginLabel} returned invalid input: ${targetValidationError}`);
}
task.input = cloneInput(decision.input);
return processPlugin(index + 1);
}).catch(error => error instanceof AssetUploadCanceledError ? cancel(error.message) : fail(error));
};
return processPlugin(0);
};
View on GitHub (pinned to 8641553a1f)
Solutions
- Fix the plugin so its respondWith promise resolves to {action: 'replace', input} or {action: 'cancel'} exactly (lowercase, no extras)
- Update the plugin to the current petal/plugin API version — only 'replace' and 'cancel' actions are accepted
- If the plugin cannot respond correctly, remove/disable it so processPlugin skips to the next plugin (a non-responding plugin is skipped)
- In your integration, catch this error from prepareAssetUpload and surface which pluginLabel misbehaved for debugging
Example fix
// before (plugin side): invalid action
respondWith({ action: "skip", input });
// after (plugin side): only replace or cancel
respondWith({ action: "replace", input });
// or
respondWith({ action: "cancel" }); Defensive patterns
Strategy: type-guard
Validate before calling
// validate a plugin decision before passing it to respondWith
const isValidDecision = (d: any) =>
d && (d.action === "cancel" ||
(d.action === "replace" && d.input && typeof d.input === "object" && Array.isArray(d.input.files)));
if (!isValidDecision(decision)) { throw new Error("decision must use action replace or cancel"); } Type guard
const isUploadDecision = (d: unknown): d is { action: "replace" | "cancel"; input?: unknown } =>
typeof d === "object" && d !== null &&
((d as any).action === "replace" || (d as any).action === "cancel"); Try / catch
try {
await prepareAssetUpload(task, context);
} catch (err) {
if (err.message.includes("returned an invalid action")) {
console.error("Misbehaving plugin:", err.message); // identify pluginLabel
} else { throw err; }
} Prevention
- Only ever pass 'replace' or 'cancel' (exact lowercase strings) as decision.action in respondWith
- Write plugin TypeScript against the current petal declarations so the action union is enforced at compile time
- Unit-test your plugin's before-upload-assets handler to assert the resolved decision shape
- Keep respondWith's promise resolving to a single, well-formed decision — never undefined or a partial object
When it happens
Trigger: A plugin's before-upload-assets listener calls respondWith with a promise resolving to {action: 'skip'}, an unknown string, or an object lacking action; or the decision resolves undefined/non-conforming after waitForDecision, and the plugin was the one that claimed the response.
Common situations: A plugin written against an older or imagined API version returning an action value not in the enum; a typo like 'Replace' or 'replaced'; a plugin resolving respondWith without setting action; two plugins interacting so a stale response object is consumed.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- Plugin ${pluginLabel} returned invalid input: ${targetValida
- Plugin ${pluginLabel} returned invalid input: ${validationEr
- invalid appearance ID
- unsupported schema version or mismatched appearance ID
- too many layers: %d
AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11).
Data as JSON: /api/errors/32836d20283c8de8.
Report an issue: GitHub.