{"record":{"id":"84d5331218557329","repo":"zylon-ai/private-gpt","slug":"provide-skill-md-either-in-files-or-skill-md","errorCode":null,"errorMessage":"Provide SKILL.md either in files or skill_md","messagePattern":"Provide SKILL\\.md either in files or skill_md","errorType":"validation","errorClass":"ValueError","httpStatus":422,"severity":"error","filePath":"private_gpt/server/skills/skill_models.py","lineNumber":74,"sourceCode":"        description=\"Instruction loading strategy.\",\n    )\n    readonly: bool = Field(\n        default=False, description=\"Readonly flag for protected skills.\"\n    )\n    skill_md: str | None = Field(\n        default=None,\n        description=\"Inline SKILL.md content (optional when files includes SKILL.md).\",\n    )\n    files: list[SkillFileInput] = Field(\n        default_factory=list,\n        description=\"Optional uploaded files for this skill version.\",\n    )\n\n    @model_validator(mode=\"after\")\n    def validate_skill_md_presence(self) -> \"CreateSkillBody\":\n        has_skill_file = any(file.path == \"SKILL.md\" for file in self.files)\n        if not has_skill_file and not self.skill_md:\n            raise ValueError(\"Provide SKILL.md either in files or skill_md\")\n        return self\n\n\nclass SkillResponse(BaseModel):\n    \"\"\"Serialized skill object returned by skills endpoints.\"\"\"\n\n    id: str = Field(description=\"Unique skill identifier.\")\n    created_at: datetime = Field(description=\"Creation timestamp.\")\n    display_title: str = Field(description=\"Human display title.\")\n    latest_version: str | None = Field(\n        default=None,\n        description=\"Latest version token for this skill.\",\n    )\n    source: Literal[\"custom\", \"anthropic\", \"zylon\"] = Field(\n        description=\"Source of the skill.\"\n    )\n    type: Literal[\"skill\"] = Field(default=\"skill\", description=\"Object type.\")\n    updated_at: datetime = Field(description=\"Update timestamp.\")","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/zylon-ai/private-gpt/blob/4a030776a31a901ad80b1bf4d7faa2c1a367efbb/private_gpt/server/skills/skill_models.py#L56-L92","documentation":"Raised by a Pydantic model_validator on CreateSkillBody when creating a skill: the request must include SKILL.md content either inline via the skill_md field or as a file entry with path == 'SKILL.md' in the files list. The server rejects the request body before it reaches the skill store. It maps to an HTTP 400 from FastAPI's RequestValidationError handling.","triggerScenarios":"POST to the skills creation endpoint with neither skill_md set nor any files[] entry whose path is exactly 'SKILL.md' (e.g. {\"files\": [{\"path\": \"README.md\", ...}]} or an entirely empty body). Note the check is case-sensitive: 'skill.md' in files does not satisfy it.","commonSituations":"Clients uploading only supporting files and forgetting the manifest; typos in the path casing ('Skill.md', 'skill.md'); building the request from a zip whose SKILL.md was flattened or renamed; API version changes that moved SKILL.md from a dedicated field into the files array.","solutions":["Add an inline SKILL.md: set skill_md to the full markdown content in the request body.","Or append a file entry with path exactly 'SKILL.md' (exact case) plus its content to files.","If uploading a zip, ensure the archive contains SKILL.md at (or under a wrapper directory containing) its root before sending.","Check the client serializer for path normalization that lowercases or renames entries."],"exampleFix":"// before\n{\"display_title\": \"my-skill\", \"files\": [{\"path\": \"README.md\", \"content\": \"...\"}]}\n// after\n{\"display_title\": \"my-skill\", \"skill_md\": \"---\\nname: my-skill\\n---\\n# My skill\", \"files\": [{\"path\": \"README.md\", \"content\": \"...\"}]}","handlingStrategy":"validation","validationCode":"def has_skill_md(body: dict) -> bool:\n    return bool(body.get('skill_md')) or any(\n        f.get('path') == 'SKILL.md' for f in body.get('files', [])\n    )\n\nassert has_skill_md(payload), 'attach SKILL.md via skill_md or files[]'","typeGuard":"type SkillPayload = { skill_md?: string; files?: { path: string; content?: string }[] };\nconst isCreatable = (p: SkillPayload): boolean =>\n  Boolean(p.skill_md) || (p.files ?? []).some((f) => f.path === 'SKILL.md');","tryCatchPattern":"try {\n  await client.post('/skills', payload);\n} catch (e: any) {\n  if (e?.status === 400 && /SKILL\\.md/.test(e.detail)) {\n    payload.skill_md = buildDefaultSkillMd();\n    await client.post('/skills', payload);\n  } else throw e;\n}","preventionTips":["Build skill payloads through a helper that always injects skill_md when files lack SKILL.md.","Treat SKILL.md path matching as case-sensitive in client code.","Add a schema test asserting your serializer always emits a SKILL.md source."],"tags":["validation","pydantic","skills","api","bad-request"],"backgroundTag":null,"analyzedSha":"4a030776a31a901ad80b1bf4d7faa2c1a367efbb","analyzedAt":"2026-08-15T03:51:26.951Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}