laurent22/joplin · error · Error

Cannot convert read-only item

Error message

Cannot convert read-only item: "%s"

What it means

Thrown by the convertNoteToMarkdown command when itemIsReadOnly() returns true for a note about to be converted. Read-only status is derived from Joplin Server sharing/ownership and the sync userId — a note owned by another user (shared notebook) cannot be converted, because conversion duplicates then trashes the original, which would destroy another user's content.

Solutions

  1. Ask the notebook owner to convert the note, or to give you write ownership.
  2. Copy the note body out manually into a new Markdown note you own instead of using the convert command.
  3. If you believe you should own the note, verify the share/ownership settings on the Joplin Server.
  4. Avoid running the command on a multi-selection that includes shared notes.
Defensive patterns

Strategy: validation

Validate before calling

import { itemIsReadOnly } from './models/utils/readOnly';
if (await itemIsReadOnly(Note, ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, note.id, Setting.value('sync.userId'), shareService)) {
  // do not attempt conversion; the command will throw
  showReadOnlyWarning(note);
  return;
}

Try / catch

try {
  await commandRuntime.execute({ ... } as CommandContext, noteIds);
} catch (error) {
  if (/Cannot convert read-only item/.test(error.message)) {
    // inform the user the note is shared/read-only
    return;
  }
  throw error;
}

Prevention

When it happens

Trigger: The user selects an HTML note that belongs to a shared folder they do not own (sync.userId !== note owner) and invokes 'Convert to Markdown'. itemIsReadOnly() consults the share service and returns true, so the conversion aborts before Note.duplicate runs.

Common situations: Working in a shared Joplin Server notebook where notes are owned by a colleague; the share service reports the note as read-only for the current user; the command's enabledCondition ('!noteIsReadOnly') was bypassed (e.g. multi-select where at least one note is read-only).

Related errors


AI-assisted analysis of laurent22/joplin@683240968b (2026-08-28). Data as JSON: /api/errors/4e7655fe39727195. Report an issue: GitHub.

Appendix: source

Thrown at packages/lib/commands/convertNoteToMarkdown.ts:51

			const notes: NoteEntity[] = await Note.loadItemsByIdsOrFail(noteIds);

			try {
				const firstLockedNote = isNoteLockEnabled() ? notes.find(note => note.markup_language !== MarkupLanguage.Markdown && NoteLockNote.isLocked(note)) : null;
				if (firstLockedNote && !NoteLockSession.instance().isUnlocked()) {
					throw new Error(_('Cannot convert locked note: "%s"', firstLockedNote.title));
				}
				// Captured once so a session lock mid-run cannot fail the remaining conversions.
				const noteLockKey = firstLockedNote ? NoteLockSession.instance().decryptedKey() : null;
				let isFirst = true;
				let processedCount = 0;
				for (const note of notes) {
					if (note.markup_language === MarkupLanguage.Markdown) {
						logger.warn('Skipping item: Already Markdown.');
						continue;
					}
					if (await itemIsReadOnly(Note, ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, note.id, Setting.value('sync.userId'), context.state.shareService)) {
						throw new Error(_('Cannot convert read-only item: "%s"', note.title));
					}
					const noteIsLocked = isNoteLockEnabled() && NoteLockNote.isLocked(note);

					// A locked note converts through a full gated load and save, so the body is
					// decrypted for the conversion and the converted copy is encrypted again.
					const sourceNote = noteIsLocked ? await Note.load(note.id, { useNoteLock: true, noteLockKey }) : note;

					const markdownBody = await convertHtmlToMarkdown().execute(context, sourceNote.body);
					const newNote = await Note.duplicate(note.id);

					newNote.body = markdownBody;
					newNote.markup_language = MarkupLanguage.Markdown;
					newNote.user_created_time = note.user_created_time;
					newNote.user_updated_time = note.user_updated_time;
					newNote.updated_time = Date.now();

					const toSave = noteIsLocked ? { ...newNote, isDecrypted: true } : newNote;
					await Note.save(toSave, { autoTimestamp: false, useNoteLock: noteIsLocked, noteLockKey });

View on GitHub (pinned to 683240968b)