Skills
Named, reusable instruction sets applied server-side to chat completions.
A skill is a named set of instructions stored with your account. Naming it on a chat completion request makes GreenPT apply those instructions on the server, so a prompt you refined once can shape any conversation without your client resending — or even storing — it.
Skills created through this API are the same skills the GreenPT Apps chat offers in its composer — create one in either place and use it from both.
Skills are not an agent's tools
Agents come with tools — web search, vision, transcription — fixed capabilities of the agent runtime. The skills on this page are instruction sets you write and manage yourself, and they are one of the parts you compose an agent from: naming a skill when starting an agent run applies its instructions to the run, the same way it shapes a chat completion.
Applying a skill
Set the skills body field on a chat completion request to a single skill
name:
{
"model": "gemma4",
"messages": [
{ "role": "user", "content": "Draft a launch email for our reporting feature." }
],
"skills": "email-writer"
}The field takes one skill per request — there is no list form. It works on streaming and non-streaming requests alike, and with every model id, including the compression models: the compressed style and your skill's instructions both apply.
curl https://api.greenpt.ai/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gemma4",
"messages": [{ "role": "user", "content": "Draft a launch email for our reporting feature." }],
"skills": "email-writer"
}'const response = await fetch('https://api.greenpt.ai/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.GREENPT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gemma4',
messages: [{ role: 'user', content: 'Draft a launch email for our reporting feature.' }],
skills: 'email-writer',
}),
});With the OpenAI SDK, pass the field via extra_body:
client.chat.completions.create(
model="gemma4",
messages=[{"role": "user", "content": "Draft a launch email for our reporting feature."}],
extra_body={"skills": "email-writer"},
)Pinning a version
The field also takes an object form that pins one of the skill's published versions:
{ "skills": { "name": "email-writer", "version": 3 } }Each write that changes a skill's instructions publishes the next version —
the skill object's version field names the latest — and earlier versions
stay addressable after further edits. A bare name always applies the latest
content; pin when a request must not move with edits. Resolution is unchanged:
the name picks the skill exactly as described below, and the pin only picks
which published revision is injected. A version the skill does not have fails
the request with a 400 carrying reason: "unknown_version" — see
Errors.
Every published version records a digest of its instruction bytes, reported as
X-Skill-Content-Hash on any response the skill shaped. Pinning a version and
comparing that digest across responses are the two ways to establish that a
result came from the instructions you think it did.
How a skill is applied
- Resolve — the name is matched against your own skills first, then your
organisation's, then skills GreenPT provides. The first match wins, so your
skill shadows a GreenPT-provided one of the same name. Matching is exact and
case-sensitive, and only enabled skills resolve; a name that matches nothing
fails the request with a
400. - Inject — the resolved instructions join the request's system message,
wrapped in a
<user_skill>block placed after your own system prompt. The wrapper frames the block as guidance you chose for the request: it stays subordinate to the instructions outside it. A request without a system message gets one holding the block. - Strip —
skillsis a GreenPT-only field; it is removed before the request reaches the model provider. - Bill — the injected block's tokens are input tokens like any others.
They are included in the
usagethe response reports and billed accordingly; theX-Skill-Injected-Tokensheader tells you exactly how many.
Response headers
When a skill resolves, the response reports what was applied, on streaming and non-streaming responses alike:
| Header | Meaning |
|---|---|
X-Skill | The applied skill's name. |
X-Skill-Injected-Tokens | Tokens the injected block added to your prompt, counted with the wrapper included. |
X-Skill-Content-Hash | Digest of the skill's instructions — the authored bytes alone, without the wrapper the token count includes. Two responses carrying the same digest were shaped by identical instructions, whatever the skill was called or renamed to. |
X-Skill-Version | The published version whose bytes were injected. |
The first three accompany every resolved skill. X-Skill-Version is
conditional: it appears only when the injected bytes are a numbered published
version, and is absent when they are not — a skill written before versions
existed, or one whose stored content no longer matches the version it records.
The hash describes the bytes in either case, which is why it, rather than the
version, is the field to compare when you need to know whether two responses
got the same instructions.
No X-Skill* header appears when the request carries no skills field.
Managing skills
Skills are created and maintained at /v1/skills, authenticated with your API
key like every other endpoint. Skills you create through it belong to you;
your organisation's skills and the GreenPT catalog are visible but not
editable.
The skill object
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier, used in the management routes. |
name | string | The name used in the skills request field. |
description | string | When GreenPT should use the skill, phrased as a condition; shown wherever skills are picked, such as the GreenPT Apps switcher. |
instructions | string | The instructions injected when the skill is applied. Detail responses only. |
enabled | boolean | Whether the skill resolves on chat completions. |
scope | string | Who provides it: user (you), organisation, or greenpt. |
version | integer | Published revisions of the instructions: 1 on create, one higher after each write that changes them. null on skills last written before versions existed. |
createdAt | string | ISO 8601 timestamp. |
updatedAt | string | ISO 8601 timestamp. |
List skills
GET /v1/skillsReturns { "skills": [...] } with every skill visible to you, sorted by name:
your own (enabled or not), plus enabled skills from your organisation and from
GreenPT. List entries omit instructions; fetch a skill by id for the
full object. The same name can appear more than once when it exists in more
than one scope — the resolution order above decides which one a chat completion
uses. The list is not paginated.
Get a skill
GET /v1/skills/{id}Returns the full skill object, including instructions.
Create a skill
POST /v1/skills| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Starts with a letter or digit; letters, digits, hyphens, underscores; max 64 characters. |
description | string | No | Max 500 characters. State when the skill should be used. |
instructions | string | Yes | 1 to 20,000 characters. Markdown welcome — it reaches the model as written. |
Returns 201 with the skill object. A skill is enabled from the moment it is
created; switch it off with a PATCH if it is not ready for use. Names are
unique among your skills, and picking the same name as an organisation or
GreenPT skill is allowed — yours takes precedence when applied.
curl https://api.greenpt.ai/v1/skills \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "release-notes",
"description": "Use when announcing shipped features to customers — release notes, changelog entries, launch emails.",
"instructions": "Write release notes as short, benefit-first bullets. Name the feature, say what it lets the reader do, skip internal jargon."
}'Update a skill
PATCH /v1/skills/{id}Accepts any of name, description, instructions, and enabled — at least
one. Only your own skills can be updated. Returns 200 with the updated
object. A patch that changes instructions publishes the next version and
raises version by one; a patch that touches only the other fields, or saves
instructions identical to the active version, publishes nothing.
Delete a skill
DELETE /v1/skills/{id}Returns 204. Deleting is permanent: the skill disappears from your list and
its name becomes free for reuse. Its published versions go with it, so a
request pinning one fails as an unknown skill, and a later skill created under
the same name starts again at version 1. To retire a skill you might want
back, set enabled to false instead:
| After | DELETE | PATCH {"enabled": false} |
|---|---|---|
Listed in GET /v1/skills | No | Yes |
| Readable and editable | No | Yes |
| Resolves on completions | No | No |
| Name reusable | Yes | No |
Errors
The two surfaces answer differently. A skills failure on a chat completion
returns the same JSON error envelope as any other completion error, so an
OpenAI-compatible SDK surfaces it the way it surfaces the rest. The management
routes under /v1/skills return plain text.
On a chat completion
{
"error": {
"message": "Invalid \"skills\" parameter: Unknown version 99 of skill \"email-writer\".",
"type": "invalid_request_error",
"param": "skills",
"code": "skill_unresolved",
"reason": "unknown_version"
}
}Every such failure is a 400. Branch on code and reason rather than
matching the message, which is prose and may be reworded:
code | reason | When |
|---|---|---|
skill_param_invalid | — | skills is neither a non-empty name nor { name, version } with a positive integer version. |
skill_unresolved | unknown | No skill of that name is visible to you — including one you deleted. |
skill_unresolved | disabled | The skill exists and you can see it, but enabled is false. |
skill_unresolved | unknown_version | The skill resolved; the pinned version was never published. |
skill_unresolved | unversioned | The skill resolved but has no addressable versions, so it cannot be pinned. |
reason is present only on skill_unresolved. Disabling and deleting are
distinguishable on purpose: both stop a skill resolving, and only one of them
is something you can undo, so a client can tell a user which happened.
Authentication, credit, and rate-limit errors are checked before the skills
field, so their status codes take precedence over any of the above.
On the management routes
Plain text, with one exception: instructions past the character cap answer with JSON, because "too long" is a limit a client reacts to programmatically rather than shows verbatim.
{
"error": {
"message": "Instructions are 24310 characters; the limit is 20000.",
"code": "instructions_too_long",
"limit": 20000,
"actual": 24310
}
}Read limit from the response rather than hard-coding it, so a client does
not go stale the first time the cap moves.
| Status | When | Body |
|---|---|---|
400 | instructions are past the cap | The JSON above, carrying code, limit and actual. |
400 | A create or update body fails validation otherwise | Invalid skill: (create) or Invalid skill patch: (update), followed by one line per issue. |
401 | Missing or invalid API key | Unauthorized |
404 | The id does not exist, or names a skill you cannot see or edit | Skill not found |
409 | The name is already taken among your skills | You already have a skill named "<name>". |
503 | Skills are not switched on in this environment | Skills are not enabled |
The 503 is checked before your API key is, so it answers an unauthenticated
request too — which makes it a free way to ask an environment whether skills
are switched on at all.
Limits
| What | Limit |
|---|---|
| Skills per request | 1 |
| Name | 64 characters |
| Description | 500 characters |
| Instructions | 20,000 characters |