from decimalai.skill_router import SkillRouter
from decimalai.skills import discover_skills
router = SkillRouter(api_key="dai_sk_...")
local_skills = discover_skills(base_path="./skills")
router.sync_skills(skills=local_skills)decimalai skills sync --dir ./skillsconst options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
skills: [
{
name: '<string>',
display_name: '<string>',
description: '',
body_markdown: '<string>',
body: JSON.stringify('<string>'),
category: '<string>',
trigger_phrases: ['<string>'],
frontmatter: {},
content_hash: '<string>',
local_updated_at: '<string>',
eval_yaml_text: '<string>',
eval_yaml_hash: '<string>',
attachments: {}
}
],
author: '<string>',
install_id: '<string>',
install_label: '<string>',
conflict_policy: 'newer_wins',
response_mode: 'summary'
})
};
fetch('https://api.decimal.ai/api/v1/skills/sync', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.decimal.ai/api/v1/skills/sync",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'skills' => [
[
'name' => '<string>',
'display_name' => '<string>',
'description' => '',
'body_markdown' => '<string>',
'body' => '<string>',
'category' => '<string>',
'trigger_phrases' => [
'<string>'
],
'frontmatter' => [
],
'content_hash' => '<string>',
'local_updated_at' => '<string>',
'eval_yaml_text' => '<string>',
'eval_yaml_hash' => '<string>',
'attachments' => [
]
]
],
'author' => '<string>',
'install_id' => '<string>',
'install_label' => '<string>',
'conflict_policy' => 'newer_wins',
'response_mode' => 'summary'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.decimal.ai/api/v1/skills/sync"
payload := strings.NewReader("{\n \"skills\": [\n {\n \"name\": \"<string>\",\n \"display_name\": \"<string>\",\n \"description\": \"\",\n \"body_markdown\": \"<string>\",\n \"body\": \"<string>\",\n \"category\": \"<string>\",\n \"trigger_phrases\": [\n \"<string>\"\n ],\n \"frontmatter\": {},\n \"content_hash\": \"<string>\",\n \"local_updated_at\": \"<string>\",\n \"eval_yaml_text\": \"<string>\",\n \"eval_yaml_hash\": \"<string>\",\n \"attachments\": {}\n }\n ],\n \"author\": \"<string>\",\n \"install_id\": \"<string>\",\n \"install_label\": \"<string>\",\n \"conflict_policy\": \"newer_wins\",\n \"response_mode\": \"summary\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.decimal.ai/api/v1/skills/sync")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"skills\": [\n {\n \"name\": \"<string>\",\n \"display_name\": \"<string>\",\n \"description\": \"\",\n \"body_markdown\": \"<string>\",\n \"body\": \"<string>\",\n \"category\": \"<string>\",\n \"trigger_phrases\": [\n \"<string>\"\n ],\n \"frontmatter\": {},\n \"content_hash\": \"<string>\",\n \"local_updated_at\": \"<string>\",\n \"eval_yaml_text\": \"<string>\",\n \"eval_yaml_hash\": \"<string>\",\n \"attachments\": {}\n }\n ],\n \"author\": \"<string>\",\n \"install_id\": \"<string>\",\n \"install_label\": \"<string>\",\n \"conflict_policy\": \"newer_wins\",\n \"response_mode\": \"summary\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.decimal.ai/api/v1/skills/sync")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"skills\": [\n {\n \"name\": \"<string>\",\n \"display_name\": \"<string>\",\n \"description\": \"\",\n \"body_markdown\": \"<string>\",\n \"body\": \"<string>\",\n \"category\": \"<string>\",\n \"trigger_phrases\": [\n \"<string>\"\n ],\n \"frontmatter\": {},\n \"content_hash\": \"<string>\",\n \"local_updated_at\": \"<string>\",\n \"eval_yaml_text\": \"<string>\",\n \"eval_yaml_hash\": \"<string>\",\n \"attachments\": {}\n }\n ],\n \"author\": \"<string>\",\n \"install_id\": \"<string>\",\n \"install_label\": \"<string>\",\n \"conflict_policy\": \"newer_wins\",\n \"response_mode\": \"summary\"\n}"
response = http.request(request)
puts response.read_body{
"status": "ok",
"created": 2,
"updated": 1,
"unchanged": 5,
"pulled": 0,
"failures": []
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}Reconcile a batch of local skills with the platform
One endpoint, two consumers:
- SDK auto-discovery (CI, agent boot): defaults are right.
Send
{"skills": [...]}and readcreated/updated/unchangedcounts. - Interactive CLI (
decimalai skills sync ./skills/): passresponse_mode="diff"to get a per-skillactionsarray includingbody_markdownfor any skill where the remote version is newer (so the CLI can overwrite local files).
Conflict policy: with conflict_policy="newer_wins" (default), the
side whose timestamp is later wins — best for human-in-the-loop CLI.
For CI/SDK pipelines where the repo is the source of truth, pass
conflict_policy="local_wins" to force every push. remote_wins is
the inverse — useful for “treat the platform as canonical.”
Empty bodies are 422’d at Pydantic validation. Invalid names are
409’d at the same layer. Per-skill issues that survive validation
(e.g. a transient DB error) land in the failures array; the rest
of the batch still processes.
content_hash on each entry is optional — clients that already
computed SHA-256 (CLI walking a directory) can pass it to save a
server-side hash; clients that didn’t (SDK auto-discovery) can omit.
from decimalai.skill_router import SkillRouter
from decimalai.skills import discover_skills
router = SkillRouter(api_key="dai_sk_...")
local_skills = discover_skills(base_path="./skills")
router.sync_skills(skills=local_skills)decimalai skills sync --dir ./skillsconst options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
skills: [
{
name: '<string>',
display_name: '<string>',
description: '',
body_markdown: '<string>',
body: JSON.stringify('<string>'),
category: '<string>',
trigger_phrases: ['<string>'],
frontmatter: {},
content_hash: '<string>',
local_updated_at: '<string>',
eval_yaml_text: '<string>',
eval_yaml_hash: '<string>',
attachments: {}
}
],
author: '<string>',
install_id: '<string>',
install_label: '<string>',
conflict_policy: 'newer_wins',
response_mode: 'summary'
})
};
fetch('https://api.decimal.ai/api/v1/skills/sync', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.decimal.ai/api/v1/skills/sync",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'skills' => [
[
'name' => '<string>',
'display_name' => '<string>',
'description' => '',
'body_markdown' => '<string>',
'body' => '<string>',
'category' => '<string>',
'trigger_phrases' => [
'<string>'
],
'frontmatter' => [
],
'content_hash' => '<string>',
'local_updated_at' => '<string>',
'eval_yaml_text' => '<string>',
'eval_yaml_hash' => '<string>',
'attachments' => [
]
]
],
'author' => '<string>',
'install_id' => '<string>',
'install_label' => '<string>',
'conflict_policy' => 'newer_wins',
'response_mode' => 'summary'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.decimal.ai/api/v1/skills/sync"
payload := strings.NewReader("{\n \"skills\": [\n {\n \"name\": \"<string>\",\n \"display_name\": \"<string>\",\n \"description\": \"\",\n \"body_markdown\": \"<string>\",\n \"body\": \"<string>\",\n \"category\": \"<string>\",\n \"trigger_phrases\": [\n \"<string>\"\n ],\n \"frontmatter\": {},\n \"content_hash\": \"<string>\",\n \"local_updated_at\": \"<string>\",\n \"eval_yaml_text\": \"<string>\",\n \"eval_yaml_hash\": \"<string>\",\n \"attachments\": {}\n }\n ],\n \"author\": \"<string>\",\n \"install_id\": \"<string>\",\n \"install_label\": \"<string>\",\n \"conflict_policy\": \"newer_wins\",\n \"response_mode\": \"summary\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.decimal.ai/api/v1/skills/sync")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"skills\": [\n {\n \"name\": \"<string>\",\n \"display_name\": \"<string>\",\n \"description\": \"\",\n \"body_markdown\": \"<string>\",\n \"body\": \"<string>\",\n \"category\": \"<string>\",\n \"trigger_phrases\": [\n \"<string>\"\n ],\n \"frontmatter\": {},\n \"content_hash\": \"<string>\",\n \"local_updated_at\": \"<string>\",\n \"eval_yaml_text\": \"<string>\",\n \"eval_yaml_hash\": \"<string>\",\n \"attachments\": {}\n }\n ],\n \"author\": \"<string>\",\n \"install_id\": \"<string>\",\n \"install_label\": \"<string>\",\n \"conflict_policy\": \"newer_wins\",\n \"response_mode\": \"summary\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.decimal.ai/api/v1/skills/sync")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"skills\": [\n {\n \"name\": \"<string>\",\n \"display_name\": \"<string>\",\n \"description\": \"\",\n \"body_markdown\": \"<string>\",\n \"body\": \"<string>\",\n \"category\": \"<string>\",\n \"trigger_phrases\": [\n \"<string>\"\n ],\n \"frontmatter\": {},\n \"content_hash\": \"<string>\",\n \"local_updated_at\": \"<string>\",\n \"eval_yaml_text\": \"<string>\",\n \"eval_yaml_hash\": \"<string>\",\n \"attachments\": {}\n }\n ],\n \"author\": \"<string>\",\n \"install_id\": \"<string>\",\n \"install_label\": \"<string>\",\n \"conflict_policy\": \"newer_wins\",\n \"response_mode\": \"summary\"\n}"
response = http.request(request)
puts response.read_body{
"status": "ok",
"created": 2,
"updated": 1,
"unchanged": 5,
"pulled": 0,
"failures": []
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}Authorizations
Enter your API key (e.g. dai_sk_test_key_001)
Cookies
Body
Validated request for POST /skills/sync.
Single endpoint covers both CLI (bidirectional reconciliation) and SDK
(one-way upsert) — the difference is the response_mode flag (rich
actions array for CLI, summary counts for SDK) and the conflict_policy
(CLI defaults to newer_wins for human-in-the-loop, CI/SDK should set
local_wins because the repo is the source of truth).
1 - 200 elementsShow child attributes
Show child attributes
Author display name recorded on every SkillVersion created by this sync.
Project-local install identity (persisted by the SDK in .decimal/install.json). When present, the sync records per-install divergence state so GET /skills/installs can flag local/remote drift. Omit for back-compat: the sync then behaves exactly as before.
128Human-friendly install label (hostname, 'ci', …) for the dashboard.
200newer_wins: compare timestamps (default; right for interactive CLI). local_wins: always overwrite remote (right for CI / build artifacts). remote_wins: never overwrite local; conflicts return pulled actions.
newer_wins, local_wins, remote_wins summary: return counts only (cheap; SDK default). diff: return per-skill actions array including bodies for pulled skills (CLI).
summary, diff Response
Successful Response
"ok"Number of new skills created.
Number of existing skills with a new version (pushed).
Number of skills whose body hash matched — no-op.
Number of skills where remote was newer; caller should refresh local.
Per-skill failures with name + error.
Per-skill action list — present only when response_mode='diff'.
Show child attributes
Show child attributes