From dev-assistant
/etendo:headless — Configure EtendoRX headless API endpoints
npx claudepluginhub etendosoftware/etendo_claude_marketplace --plugin dev-assistantThis skill uses the workspace's default tool permissions.
**Arguments:** `$ARGUMENTS` (optional: endpoint name, table name, or description)
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides agent creation for Claude Code plugins with file templates, frontmatter specs (name, description, model), triggering examples, system prompts, and best practices.
Arguments: $ARGUMENTS (optional: endpoint name, table name, or description)
First, read skills/etendo-_guidelines/SKILL.md and skills/etendo-_context/SKILL.md.
For the full EtendoRX API reference (entities, endpoints, CSRF handling), read references/etendo-headless.md. For the classic DataSource API and FormInit, read references/etendo-api-guide.md.
This command creates or modifies EtendoRX headless endpoints -- REST API endpoints that expose Etendo data without CSRF, using JWT Bearer or Basic Auth.
Key concept: Each endpoint (ETAPI_OPENAPI_REQ) is mapped to one or more Tabs (ETRX_OPENAPI_TAB). Each tab exposes specific fields (ETRX_ENTITY_FIELD). The endpoint name becomes the URL segment:
GET/POST/PUT /etendo/sws/com.etendoerp.etendorx.datasource/{EndpointName}
Verify the module is installed:
SELECT javapackage FROM ad_module WHERE javapackage = 'com.etendoerp.etendorx' AND isactive = 'Y';
If not present: "EtendoRX module is not installed. Add implementation('com.etendoerp:etendorx:[version]') to build.gradle and run ./gradlew update.database."
Check EtendoRX service is running (if Docker):
docker ps --filter name=etendo-etendorx --format "{{.Names}} {{.Status}}"
Based on $ARGUMENTS:
list -> show existing endpoints for the active modulecreate {name} or blank -> create a new endpointalter {name} / add-field {name} -> add fields to existing endpointtest {name} -> test an endpoint with a curl callList existing endpoints:
SELECT r.name AS endpoint, t.name AS tab_name, tbl.tablename,
COUNT(f.etrx_entity_field_id) AS field_count
FROM etapi_openapi_req r
JOIN etrx_openapi_tab t ON t.etapi_openapi_req_id = r.etapi_openapi_req_id
JOIN ad_tab tab ON tab.ad_tab_id = t.ad_tab_id
JOIN ad_table tbl ON tbl.ad_table_id = tab.ad_table_id
LEFT JOIN etrx_entity_field f ON f.etrx_openapi_tab_id = t.etrx_openapi_tab_id
WHERE r.ad_module_id = '{AD_MODULE_ID}'
GROUP BY r.name, t.name, tbl.tablename
ORDER BY r.name;
Ask conversationally:
Endpoint name (becomes URL segment): e.g. MyCustomers
/sws/com.etendoerp.etendorx.datasource/MyCustomersWhich Tab to expose? List available tabs:
SELECT t.ad_tab_id, t.name, tbl.tablename
FROM ad_tab t
JOIN ad_table tbl ON tbl.ad_table_id = t.ad_table_id
WHERE t.ad_module_id = '{AD_MODULE_ID}'
ORDER BY t.name;
(Or allow specifying a core tab like C_Order tab 186)
Which fields to expose? List columns for the selected tab:
SELECT c.columnname, c.name, c.ad_reference_id
FROM ad_column c
JOIN ad_table tbl ON tbl.ad_table_id = c.ad_table_id
WHERE tbl.ad_table_id = '{table_id}'
ORDER BY c.columnname;
Options:
Operations allowed: GET (list/fetch), POST (create), PUT (update)? Default: all three.
Use the RegisterHeadlessEndpoint webhook (requires Tomcat running + API key):
ETENDO_URL=$(cat .etendo/context.json | python3 -c "import sys,json; print(json.load(sys.stdin).get('etendoUrl','http://localhost:8080/etendo'))")
# moduleId is NOT stored in context.json — resolve it from the module's javapackage:
MODULE_JP=$(cat .etendo/context.json | python3 -c "import sys,json; print(json.load(sys.stdin).get('module',''))")
MODULE_ID=$(docker exec etendo-db-1 psql -U {bbdd.user} -d {bbdd.sid} -t -c \
"SELECT ad_module_id FROM ad_module WHERE javapackage = '${MODULE_JP}';" | tr -d ' ')
# Register by table name (auto-resolves to first header tab):
RESP=$(curl -s -X POST "${ETENDO_URL}/webhooks/RegisterHeadlessEndpoint" \
-H "Authorization: Bearer ${ETENDO_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"RequestName\": \"{EndpointName}\",
\"ModuleID\": \"${MODULE_ID}\",
\"TableName\": \"{db_table_name}\",
\"Description\": \"{description}\"
}")
echo $RESP
# Or register by explicit TabID:
RESP=$(curl -s -X POST "${ETENDO_URL}/webhooks/RegisterHeadlessEndpoint" \
-H "Authorization: Bearer ${ETENDO_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"RequestName\": \"{EndpointName}\",
\"ModuleID\": \"${MODULE_ID}\",
\"TabID\": \"{AD_TAB_ID}\"
}")
echo $RESP
If Tomcat is not running, fall back to SQL. The full registration requires 5 tables:
Schema gotchas (do NOT violate these):
ETAPI_OPENAPI_REQ.typeis NOT NULL — always pass'ETRX_Tab'for tab-backed endpoints.ETRX_OPENAPI_TABandETRX_ENTITY_FIELDdo NOT have anamecolumn.ETAPI_OPENAPI_FLOWPOINTcolumns areget/post/put/getbyid(NOTisget/ispostetc.).- Use
createdby/updatedby = '0'(System user), not'100'.
cat > /tmp/register_headless.sql << 'EOF'
DO $$
DECLARE
v_req_id TEXT := get_uuid();
v_oapi_tab_id TEXT := get_uuid();
v_flow_id TEXT := get_uuid();
v_flowpoint_id TEXT := get_uuid();
v_module_id TEXT := '{AD_MODULE_ID}';
v_tab_id TEXT := '{AD_TAB_ID}';
BEGIN
-- 1. Create the endpoint (request)
INSERT INTO ETAPI_OPENAPI_REQ (
ETAPI_OPENAPI_REQ_ID, AD_CLIENT_ID, AD_ORG_ID, ISACTIVE, CREATED, CREATEDBY, UPDATED, UPDATEDBY,
NAME, DESCRIPTION, AD_MODULE_ID, TYPE
) VALUES (
v_req_id, '0', '0', 'Y', now(), '0', now(), '0',
'{EndpointName}', '{description}', v_module_id, 'ETRX_Tab'
);
-- 2. Link the endpoint to the AD tab (no 'name' column in this table!)
INSERT INTO ETRX_OPENAPI_TAB (
ETRX_OPENAPI_TAB_ID, AD_CLIENT_ID, AD_ORG_ID, ISACTIVE, CREATED, CREATEDBY, UPDATED, UPDATEDBY,
ETAPI_OPENAPI_REQ_ID, AD_TAB_ID, AD_MODULE_ID
) VALUES (
v_oapi_tab_id, '0', '0', 'Y', now(), '0', now(), '0',
v_req_id, v_tab_id, v_module_id
);
-- 3. Expose fields (one INSERT per field; no 'name' column — linked via ad_field_id)
-- Get field IDs first:
-- SELECT f.ad_field_id, f.name, c.columnname
-- FROM ad_field f JOIN ad_column c ON c.ad_column_id = f.ad_column_id
-- WHERE f.ad_tab_id = '{AD_TAB_ID}' ORDER BY f.seqno;
INSERT INTO ETRX_ENTITY_FIELD (
ETRX_ENTITY_FIELD_ID, AD_CLIENT_ID, AD_ORG_ID, ISACTIVE, CREATED, CREATEDBY, UPDATED, UPDATEDBY,
ETRX_OPENAPI_TAB_ID, AD_FIELD_ID, AD_MODULE_ID
) VALUES (
get_uuid(), '0', '0', 'Y', now(), '0', now(), '0',
v_oapi_tab_id, '{AD_FIELD_ID_1}', v_module_id
);
-- Repeat for each additional field...
-- 4. Create a flow to group the endpoint
INSERT INTO ETAPI_OPENAPI_FLOW (
ETAPI_OPENAPI_FLOW_ID, AD_CLIENT_ID, AD_ORG_ID, ISACTIVE, CREATED, CREATEDBY, UPDATED, UPDATEDBY,
NAME, AD_MODULE_ID
) VALUES (
v_flow_id, '0', '0', 'Y', now(), '0', now(), '0',
'{EndpointName} Flow', v_module_id
);
-- 5. Create a flowpoint to enable HTTP methods (columns: get, post, put, getbyid — boolean Y/N)
INSERT INTO ETAPI_OPENAPI_FLOWPOINT (
ETAPI_OPENAPI_FLOWPOINT_ID, AD_CLIENT_ID, AD_ORG_ID, ISACTIVE, CREATED, CREATEDBY, UPDATED, UPDATEDBY,
ETAPI_OPENAPI_FLOW_ID, ETAPI_OPENAPI_REQ_ID,
GET, POST, PUT, GETBYID
) VALUES (
v_flowpoint_id, '0', '0', 'Y', now(), '0', now(), '0',
v_flow_id, v_req_id,
'Y', 'Y', 'Y', 'Y' -- enable GET list, POST create, PUT update, GET by ID
);
RAISE NOTICE 'Endpoint registered: % (req_id=%)', '{EndpointName}', v_req_id;
END $$;
EOF
docker cp /tmp/register_headless.sql etendo-db-1:/tmp/register_headless.sql
docker exec etendo-db-1 psql -U {bbdd.user} -d {bbdd.sid} -f /tmp/register_headless.sql
Execute the SQL. Then test the endpoint immediately:
# Get a JWT — use System Administrator (role "0") for admin operations
ETENDO_TOKEN=$(curl -s -X POST "${ETENDO_URL}/sws/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin","role":"0"}' \
| python3 -c "import sys,json; data=json.loads(sys.stdin.buffer.read().decode('utf-8','replace')); print(data.get('token',''))")
# Test GET
curl -s -H "Authorization: Bearer ${ETENDO_TOKEN}" \
"${ETENDO_URL}/sws/com.etendoerp.etendorx.datasource/{EndpointName}?_startRow=0&_endRow=5" \
| python3 -m json.tool
If the test returns 401/403: explain SWS access configuration requirement (must create etrx_rx_services_access record linking user -> auth service). See references/headless-setup.sql.
Important: export.database requires Tomcat to be stopped first.
JAVA_HOME=$(/usr/libexec/java_home -v 17 2>/dev/null || echo "$JAVA_HOME")
JAVA_HOME=${JAVA_HOME} ./gradlew resources.down
JAVA_HOME=${JAVA_HOME} ./gradlew export.database -Dmodule={javapackage} > /tmp/etendo-export.log 2>&1
tail -5 /tmp/etendo-export.log
# Bring services back up after export:
JAVA_HOME=${JAVA_HOME} ./gradlew resources.up
+ Endpoint configured: {EndpointName}
URL: GET/POST/PUT http://localhost:8080/{context}/sws/com.etendoerp.etendorx.datasource/{EndpointName}
Auth: Bearer {JWT} or Basic {base64(user:pass)}
Fields: {field1}, {field2}, ...
Example GET:
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/{context}/sws/com.etendoerp.etendorx.datasource/{EndpointName}"
To use from code (see poc/server/src/etendo.js):
const client = makeHeadlessClient('{etendoUrl}', authorization)
client.get('{EndpointName}', 'id=="{id}"')