Create Ontology Change (writeback)
curl --request POST \
--url https://app.textql.com/v2/sandcastles/{id}/ontology/changes \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"title": "<string>",
"description": "<string>",
"draft": false,
"change_number": 123
}
'import requests
url = "https://app.textql.com/v2/sandcastles/{id}/ontology/changes"
payload = {
"title": "<string>",
"description": "<string>",
"draft": False,
"change_number": 123
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({title: '<string>', description: '<string>', draft: false, change_number: 123})
};
fetch('https://app.textql.com/v2/sandcastles/{id}/ontology/changes', 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://app.textql.com/v2/sandcastles/{id}/ontology/changes",
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([
'title' => '<string>',
'description' => '<string>',
'draft' => false,
'change_number' => 123
]),
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://app.textql.com/v2/sandcastles/{id}/ontology/changes"
payload := strings.NewReader("{\n \"title\": \"<string>\",\n \"description\": \"<string>\",\n \"draft\": false,\n \"change_number\": 123\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://app.textql.com/v2/sandcastles/{id}/ontology/changes")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"title\": \"<string>\",\n \"description\": \"<string>\",\n \"draft\": false,\n \"change_number\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.textql.com/v2/sandcastles/{id}/ontology/changes")
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 \"title\": \"<string>\",\n \"description\": \"<string>\",\n \"draft\": false,\n \"change_number\": 123\n}"
response = http.request(request)
puts response.read_body{
"change_id": "<string>",
"change_number": 123,
"status": "open",
"git_ref": "<string>",
"has_conflicts": true,
"conflicts": "<string>",
"auto_approved": true,
"diffs": [
{
"name": "<string>",
"old_path": "<string>",
"new_path": "<string>",
"additions": 123,
"deletions": 123,
"is_new": true,
"is_delete": true,
"is_rename": true,
"is_binary": true
}
],
"raw_diff": "<string>"
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "Authentication required"
}
}{
"error": {
"code": "permission_denied",
"message": "Insufficient permissions"
}
}{
"error": {
"code": "not_found",
"message": "Resource not found"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded"
}
}{
"error": {
"code": "internal",
"message": "Internal server error"
}
}Sandcastles
Create Ontology Change (Writeback)
Persist a sandcastle’s edits to /sandbox/files/library back to the org’s Ontology as a reviewable change.
POST
/
v2
/
sandcastles
/
{id}
/
ontology
/
changes
Create Ontology Change (writeback)
curl --request POST \
--url https://app.textql.com/v2/sandcastles/{id}/ontology/changes \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"title": "<string>",
"description": "<string>",
"draft": false,
"change_number": 123
}
'import requests
url = "https://app.textql.com/v2/sandcastles/{id}/ontology/changes"
payload = {
"title": "<string>",
"description": "<string>",
"draft": False,
"change_number": 123
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({title: '<string>', description: '<string>', draft: false, change_number: 123})
};
fetch('https://app.textql.com/v2/sandcastles/{id}/ontology/changes', 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://app.textql.com/v2/sandcastles/{id}/ontology/changes",
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([
'title' => '<string>',
'description' => '<string>',
'draft' => false,
'change_number' => 123
]),
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://app.textql.com/v2/sandcastles/{id}/ontology/changes"
payload := strings.NewReader("{\n \"title\": \"<string>\",\n \"description\": \"<string>\",\n \"draft\": false,\n \"change_number\": 123\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://app.textql.com/v2/sandcastles/{id}/ontology/changes")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"title\": \"<string>\",\n \"description\": \"<string>\",\n \"draft\": false,\n \"change_number\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.textql.com/v2/sandcastles/{id}/ontology/changes")
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 \"title\": \"<string>\",\n \"description\": \"<string>\",\n \"draft\": false,\n \"change_number\": 123\n}"
response = http.request(request)
puts response.read_body{
"change_id": "<string>",
"change_number": 123,
"status": "open",
"git_ref": "<string>",
"has_conflicts": true,
"conflicts": "<string>",
"auto_approved": true,
"diffs": [
{
"name": "<string>",
"old_path": "<string>",
"new_path": "<string>",
"additions": 123,
"deletions": 123,
"is_new": true,
"is_delete": true,
"is_rename": true,
"is_binary": true
}
],
"raw_diff": "<string>"
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "Authentication required"
}
}{
"error": {
"code": "permission_denied",
"message": "Insufficient permissions"
}
}{
"error": {
"code": "not_found",
"message": "Resource not found"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded"
}
}{
"error": {
"code": "internal",
"message": "Internal server error"
}
}This is the writeback half of the Ontology. The library is mounted into
every sandcastle at
/sandbox/files/library, pruned to your OWNERS
permissions. Reads happen over the filesystem; writes back to canonical,
versioned org context happen here — as a reviewable change, not a silent commit.Workflow
1
2
(Optional) Preview the diff
Call Diff Ontology to confirm what
will be written back before authoring a change.
3
Create the change
POST /v2/sandcastles/:id/ontology/changes with a title and description.
The change is submitted OPEN for admin review (or DRAFT if you set
draft: true).4
Handle conflicts / iterate
If
has_conflicts is true the change stays RESERVED and the session’s
library/ is re-materialized with .rej markers. Resolve them and re-submit
with the same change_number to file a new changeset.Driving this from an agent loop
If you let an LLM decide when to write back, give the tool a description that teaches the stage-then-change workflow — otherwise models try to “save” before editing, or file empty changes. A description that mirrors the in-product tool works well:Writeback your changes under /sandbox/files/library to the organization's
Ontology by creating or updating a change. Changes are the only way to
persist library changes; they are submitted for admin review.
Edit the files first and make sure all changes are written to the local
library/ directory — only then create a change (creating one with no changes
fails). Write a descriptive title (<=50 chars) and a Markdown description.
To revise an existing change, pass its change_number (this creates a new
changeset). If the response reports merge conflicts, resolve the .rej files and
re-submit with the same change_number. Do not create .bak backup files in the
library before creating a change.
Permissions are enforced twice: the mount you edited was already pruned to your
OWNERS access, and every changed path is re-validated at merge — a change can
never widen access. If an auto-approve rule matches, status comes back
approved and the change is already live.Authorizations
API key or JWT token
Path Parameters
Sandbox ID
Body
application/json
Short summary (≤50 chars). Required for a new change.
Maximum string length:
50Markdown explanation of the changes. Required for a new change.
File as DRAFT instead of OPEN (not yet ready for review).
Set to revise an existing change (creates a new changeset).
Response
Change created or updated
Change lifecycle state after submission.
Available options:
open, draft, approved, reserved Human-readable conflict view (present when has_conflicts).
Show child attributes
Show child attributes