{"record":{"id":"3a264895c7a85299","repo":"paperclipai/paperclip","slug":"verb-requires-verb-reply-an-existing-conversation-a-parent","errorCode":null,"errorMessage":"${verb} requires ${verb === \"reply\" ? \"an existing conversation\" : \"a parent task and a new conversation\"}","messagePattern":"(.+?) requires (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cli/src/commands/client/email.ts","lineNumber":41,"sourceCode":"      { json: true },\n    );\n  });\n  for (const verb of [\"send\", \"reply\"] as const) {\n    addCommonClientOptions(\n      email\n        .command(verb)\n        .requiredOption(\n          \"--file <path>\",\n          \"JSON request file, including a stable idempotencyKey\",\n        ),\n      { includeCompany: true },\n    ).action(async (opts: BaseClientOptions & { file: string }) => {\n      const ctx = resolveCommandContext(opts, { requireCompany: true });\n      const input = emailSendSchema.parse(\n        JSON.parse(await readFile(opts.file, \"utf8\")),\n      );\n      if ((verb === \"reply\") !== Boolean(input.conversationId))\n        throw new Error(\n          `${verb} requires ${verb === \"reply\" ? \"an existing conversation\" : \"a parent task and a new conversation\"}`,\n        );\n      printOutput(\n        await ctx.api.post(`/api/companies/${ctx.companyId}/email/send`, input),\n        { json: true },\n      );\n    });\n  }\n  addCommonClientOptions(\n    email.command(\"thread\").argument(\"<issueId>\", \"Email task ID\"),\n    { includeCompany: true },\n  ).action(async (issueId: string, opts: BaseClientOptions) => {\n    const ctx = resolveCommandContext(opts, { requireCompany: true });\n    printOutput(\n      await ctx.api.get(\n        `/api/companies/${ctx.companyId}/email/tasks/${encodeURIComponent(issueId)}`,\n      ),\n      { json: true },","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/cli/src/commands/client/email.ts#L23-L59","documentation":"The Paperclip CLI email command enforces an invariant between the subcommand verb and the payload: 'reply' must carry a conversationId pointing at an existing conversation, while 'send' must NOT carry one (it creates a new conversation under a parent task). The command derives this from `(verb === \"reply\") !== Boolean(input.conversationId)` after parsing the JSON input file, and throws this Error when the two disagree.","triggerScenarios":"Running `paperclip email reply --file msg.json` where msg.json omits `conversationId`, or running `paperclip email send --file msg.json` where msg.json unexpectedly includes a `conversationId`. The parsed payload fails the verb/payload consistency check in cli/src/commands/client/email.ts:41.","commonSituations":"Reusing a reply JSON file for a fresh send (stale conversationId left in the file); hand-writing a reply payload and forgetting the conversationId; copy-pasting a send example and adding conversationId thinking it is required; scripting bulk email where a template mixes both cases.","solutions":["For `reply`: add the `conversationId` of the existing conversation to the JSON input file.","For `send`: remove the `conversationId` field from the JSON input file so a new conversation is created under the parent task.","Check which verb you actually intend and pass the matching `--file` payload; keep separate templates for send and reply."],"exampleFix":"// before (email send fails)\n{ \"taskId\": \"t_123\", \"conversationId\": \"c_456\", \"body\": \"hi\" }\n// after\n{ \"taskId\": \"t_123\", \"body\": \"hi\" }","handlingStrategy":"validation","validationCode":"function validateEmailPayload(verb, input) {\n  if ((verb === \"reply\") !== Boolean(input.conversationId)) {\n    throw new Error(verb === \"reply\"\n      ? \"reply requires conversationId of an existing conversation\"\n      : \"send must not include conversationId (a new conversation is created)\");\n  }\n}","typeGuard":"const isReplyPayload = (input) => typeof input.conversationId === \"string\" && input.conversationId.length > 0;","tryCatchPattern":"try {\n  await api.post(`/api/companies/${companyId}/email/send`, input);\n} catch (err) {\n  if (err.message.includes(\"requires\")) {\n    console.error(\"Verb/payload mismatch: reply needs conversationId; send must omit it.\");\n  } else throw err;\n}","preventionTips":["Keep separate JSON templates for send and reply; never reuse one for both.","Strip conversationId before building a send payload.","For replies, resolve the conversationId from the thread you are answering before writing the file.","Validate payloads with the emailSendSchema locally before invoking the CLI."],"tags":["cli","validation","email"],"backgroundTag":"invalid-argument-value","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}