From dev-assistant
Etendo Dev Assistant — Shared Guidelines. Cross-cutting conventions that apply to ALL /etendo:* skills.
npx claudepluginhub etendosoftware/etendo_claude_marketplace --plugin dev-assistantThis skill uses the workspace's default tool permissions.
This file is NOT a user-facing command. It is read by all `/etendo:*` skills to ensure consistent behavior across the entire Dev Assistant.
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.
This file is NOT a user-facing command. It is read by all /etendo:* skills to ensure consistent behavior across the entire Dev Assistant.
Hierarchy of shared files:
_guidelines(this file) — conventions, patterns, output format_context— project detection, module resolution, DB connection, Gradle tasks_webhooks— webhook invocation patterns, specific webhook parameters, ID extraction
All skills that call webhooks or headless endpoints need these variables. Read them from .etendo/context.json at the start of any operation:
ETENDO_URL=$(cat .etendo/context.json | python3 -c "import sys,json; print(json.load(sys.stdin).get('etendoUrl','http://localhost:8080/etendo'))")
DB_PREFIX=$(cat .etendo/context.json | python3 -c "import sys,json; print(json.load(sys.stdin).get('dbPrefix',''))")
MODULE_JP=$(cat .etendo/context.json | python3 -c "import sys,json; print(json.load(sys.stdin).get('module',''))")
Then obtain a Bearer token (required for both webhooks and headless endpoints):
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',''))")
MODULE_ID is NOT stored in context.json — resolve it at runtime:
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 ' ')
For local DB (when docker_com.etendoerp.docker_db is not true):
MODULE_ID=$(psql -U {bbdd.user} -d {bbdd.sid} -h localhost -p {bbdd.port} -t -c \
"SELECT ad_module_id FROM ad_module WHERE javapackage = '${MODULE_JP}';" | tr -d ' ')
Gradle requires Java 17. Without the correct JAVA_HOME, builds fail with "Unsupported class file major version". Always detect before running any Gradle command:
JAVA_HOME=$(/usr/libexec/java_home -v 17 2>/dev/null || echo "$JAVA_HOME")
Then prefix every Gradle call:
JAVA_HOME=${JAVA_HOME} ./gradlew {task}
ALL long-running ./gradlew calls MUST be delegated to a general-purpose subagent. Never run Gradle directly in the main agent. Long Gradle tasks (smartbuild, compile.complete, install, export.database, generate.entities) can take minutes and will consume the main context window.
Pattern:
Agent(
subagent_type="general-purpose",
prompt="Run the following command in {working_dir}: JAVA_HOME=... ./gradlew {task} > /tmp/etendo-{task}.log 2>&1; EXIT=$?; tail -20 /tmp/etendo-{task}.log; exit $EXIT"
)
The subagent runs the command, returns the last lines of output, and exits with the Gradle exit code. Inspect the result and diagnose errors in the main agent.
All ./gradlew calls redirect output to /tmp/etendo-{task}.log. Read the log only on failure — this keeps the console clean and makes error diagnosis easier.
JAVA_HOME=${JAVA_HOME} ./gradlew smartbuild > /tmp/etendo-smartbuild.log 2>&1
tail -5 /tmp/etendo-smartbuild.log
On failure, diagnose with:
grep -E "ERROR|Exception|FAILED" /tmp/etendo-{task}.log | tail -30
Common errors across all Gradle tasks:
| Error | Cause | Fix |
|---|---|---|
Unsupported class file major version | Wrong Java version | Set JAVA_HOME to Java 17 |
Connection refused | DB not running | ./gradlew resources.up or start PostgreSQL |
Authentication failed | Wrong bbdd.* credentials | Check gradle.properties |
Could not resolve | Invalid GitHub token | Check githubToken in gradle.properties |
invalid mount path | setup not run before Docker | Run ./gradlew setup first |
OutOfMemoryError | JVM heap too small | Add org.gradle.jvmargs=-Xmx4g to gradle.properties |
export.database requires Tomcat to be stopped. Always bracket it with resources.down / resources.up:
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
Wait for containers to be healthy before running smartbuild or other webhook-dependent operations.
These apply everywhere in Etendo development:
| Context | Convention | Example |
|---|---|---|
| Database (tables, columns) | Lowercase, words separated by _ | smft_course_edition |
| Application Dictionary (window/tab/field names) | Each word capitalized, separated by spaces | Course Edition |
| DB prefix | 3-7 uppercase letters only, no numbers | SMFT, COPDEV |
| Java package | Lowercase dot-separated, reverse domain | com.smf.tutorial |
| Java class | PascalCase | CourseEditionEventHandler |
| Search keys | {PREFIX}_{DescriptiveName} in CamelCase | SMFT_ExpireEnrollments |
Language rule: All Application Dictionary configuration (names, descriptions, help texts) must be in English, even if the user communicates in another language. This ensures consistency across translations and avoids encoding issues.
Extension columns (EM_ prefix): When a module adds a column to a table owned by a different module (not just core tables — any other module), the column name must be prefixed with EM_{PREFIX}_. The CreateColumn webhook handles this automatically — always pass the column name without your module prefix. Example: pass "Is_Course", the webhook creates EM_SMFT_Is_Course. TableDir references (ref 19) are not allowed on extension columns — use Search (ref 30) instead. See alter-db skill for details.
Most webhooks use PascalCase parameters: ModuleID, Name, DBPrefix, Description.
The exception is CreateColumn, which uses camelCase: tableID, columnNameDB, moduleID, canBeNull.
Using the wrong case causes silent failures — always copy parameter names exactly from the examples in _webhooks.
Webhooks return IDs in two formats. Use the correct regex (documented in _webhooks skill):
# Universal (handles both formats — recommended):
ID=$(echo $RESP | python3 -c "import sys,json,re; r=json.load(sys.stdin); m=re.search(r\"ID:?\s*'?([A-F0-9a-f]{32})'?\",r.get('message','')); print(m.group(1) if m else r.get('error','FAIL'))")
Always check for errors:
if [ "$ID" = "FAIL" ] || [ -z "$ID" ]; then
echo "ERROR: $RESP"
# Stop and diagnose
fi
Every skill ends with a result summary using this consistent format:
+ {Action verb} {subject}
{Key detail 1}: {value}
{Key detail 2}: {value}
Next steps:
/etendo:{skill1} -> {what it does}
/etendo:{skill2} -> {what it does}
The + prefix signals success. Keep it concise — the summary should fit in a terminal without scrolling.
Always show a plan and ask for confirmation before:
update.database or install (modify the database)export.database (modifies XML files)Exception: read-only operations (queries, status checks, listing) do not need confirmation.
Not all Etendo installations use Docker. When running commands that depend on Docker:
gradle.properties for docker_com.etendoerp.docker_db=true and docker_com.etendoerp.tomcat=truedocker exec -i etendo-db-1 psql -U {bbdd.user} -d {bbdd.sid}psql -U {bbdd.user} -d {bbdd.sid} -h localhost -p {bbdd.port}docker exec etendo-tomcat-1 sh -c 'tail -n 100 /usr/local/tomcat/logs/openbravo.log'tail -n 100 $CATALINA_HOME/logs/openbravo.logAfter smartbuild or compile.complete deploys the WAR, Tomcat behavior differs:
docker_com.etendoerp.tomcat=true): Tomcat detects the updated WAR and auto-reloads after a short delay (~30-60s). No manual action needed — just wait.false): Tomcat does NOT auto-reload. The user must restart Tomcat manually for changes to take effect.Always inform the user which case applies after a successful build.
Never use heredoc with docker exec — it hangs indefinitely.
Correct pattern: write to /tmp, copy to container, then execute:
cat > /tmp/my_script.sql << 'EOF'
SELECT 1;
EOF
docker cp /tmp/my_script.sql etendo-db-1:/tmp/my_script.sql
docker exec etendo-db-1 psql -U {bbdd.user} -d {bbdd.sid} -f /tmp/my_script.sql
For short single-line queries, inline -c is acceptable:
docker exec etendo-db-1 psql -U {bbdd.user} -d {bbdd.sid} -t -c "SELECT 1;"
After creating or modifying tables, columns, or views, run this mandatory sequence:
# 1. TableChecker — detect column changes
curl -s -X POST "${ETENDO_URL}/webhooks/CheckTablesColumnHook" \
-H "Authorization: Bearer ${ETENDO_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"TableID\": \"${TABLE_ID}\"}"
# 2. SyncTerms — synchronize terms
curl -s -X POST "${ETENDO_URL}/webhooks/SyncTerms" \
-H "Authorization: Bearer ${ETENDO_TOKEN}" \
-H "Content-Type: application/json" -d '{}'
# 3. ElementsHandler — auto-correct elements
curl -s -X POST "${ETENDO_URL}/webhooks/ElementsHandler" \
-H "Authorization: Bearer ${ETENDO_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"TableID\": \"${TABLE_ID}\", \"Mode\": \"READ_ELEMENTS\"}"
Never skip or reorder these steps. See alter-db and window skills for full details.
AD_ELEMENT sync: The CreateColumn webhook creates the physical column + AD_COLUMN + AD_ELEMENT in one call. But if columns were created directly via SQL (ALTER TABLE ADD COLUMN), only the physical column exists. CheckTablesColumnHook creates the missing AD_COLUMN, but not the AD_ELEMENT. Without it, RegisterFields fails with NPE. See the element sync SQL in the alter-db skill.
If the com.etendoerp.copilot.devassistant module is not installed, these webhooks won't be available. In that case, ask the user to perform these steps manually from the Etendo UI (Application Dictionary → Synchronize Terminology, etc.).
Both webhooks and headless endpoints use Bearer token authentication (same token from /sws/login). The difference is the URL path and the concept:
Headless endpoints are automatically generated from the EtendoRX configuration (flows, flowpoints, tabs, fields). There is no custom Java code behind them — they simply perform CRUD operations on the tab they're mapped to.
{ETENDO_URL}/sws/com.etendoerp.etendorx.datasource/{EndpointName}
Use them to: query data, check if records exist, create/update individual records.
Webhooks are custom Java classes written by a developer. They can do simple or complex things in a single call (e.g., CreateAndRegisterTable creates the physical table + registers in AD_TABLE + adds base columns, all at once).
{ETENDO_URL}/webhooks/{WebhookName}
Use them for: complex operations that involve multiple steps, validations, or side effects.
All calls (both webhooks and headless) use the same Bearer token:
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',''))")
Use Authorization: Bearer ${ETENDO_TOKEN} for every call. Do NOT use ?apikey=....
For business data that requires a specific org/role context, use a non-system role. Query available roles:
SELECT ad_role_id, name FROM ad_role WHERE isactive = 'Y' ORDER BY name;
# GET list (with optional q filter)
curl -s -H "Authorization: Bearer ${ETENDO_TOKEN}" \
"${ETENDO_URL}/sws/com.etendoerp.etendorx.datasource/{Endpoint}"
# GET by ID
curl -s -H "Authorization: Bearer ${ETENDO_TOKEN}" \
"${ETENDO_URL}/sws/com.etendoerp.etendorx.datasource/{Endpoint}/{id}"
# POST (create)
curl -s -X POST -H "Authorization: Bearer ${ETENDO_TOKEN}" \
-H "Content-Type: application/json" \
"${ETENDO_URL}/sws/com.etendoerp.etendorx.datasource/{Endpoint}" \
-d '{"field1":"value1"}'
# PUT (update)
curl -s -X PUT -H "Authorization: Bearer ${ETENDO_TOKEN}" \
-H "Content-Type: application/json" \
"${ETENDO_URL}/sws/com.etendoerp.etendorx.datasource/{Endpoint}/{id}" \
-d '{"field1":"newValue"}'
q parameter)| Operator | Meaning | Example |
|---|---|---|
== | Equals | q=name==MyWebhook |
=ic= | Case-insensitive contains | q=name=ic=webhook |
=sw= | Starts with | q=name=sw=Sales |
=ge= / =le= | Greater/less than or equal | q=created=ge=2024-01-01 |
moduleHeader — list installed modulesmoduleDBPrefix — list module DB prefixesWebhook / WebhookParam — manage webhooks via CRUDTable / Column — inspect AD metadata/etendo:headless vs /etendo:flow — when to use eachBoth skills configure EtendoRX headless API endpoints, but they operate differently:
| Aspect | /etendo:headless | /etendo:flow |
|---|---|---|
| Primary method | Webhook RegisterHeadlessEndpoint | Direct SQL |
| Complexity | Simple — 1 webhook call | Full control — 5 SQL INSERTs |
| Fields | Not exposed automatically (webhook limitation) | You choose exactly which fields to expose |
| Flow + Flowpoint | Not created automatically | Created as part of the flow |
| Best for | Quick endpoint for a standard tab | Full OpenAPI configuration, multiple tabs, custom field sets |
| Fallback when Tomcat down | Full SQL block in skill (5 tables) | Always uses SQL — no webhook dependency |
Decision rule:
/etendo:headless when: you want a simple endpoint for a single tab, you don't need fine-grained field control, and Tomcat is running./etendo:flow when: you need to expose specific fields only, configure multiple endpoints in a group, or Tomcat is not available.
/etendo:headlessusesRegisterHeadlessEndpointwhich createsETAPI_OPENAPI_REQ+ETRX_OPENAPI_TABbut does NOT add fields or a flow. The endpoint works for basic GET/POST/PUT but won't appear in the OpenAPI docs until a flow and flowpoint are added. If you need the full configuration, use/etendo:flowinstead.
Skills must be resilient. The com.etendoerp.copilot.devassistant module may not be installed, or Tomcat may be down. Follow this priority order:
| Priority | Method | When to use |
|---|---|---|
| 1. Webhooks | POST /webhooks/{Name} | Preferred — handles validations, triggers, and multi-step logic in one call |
| 2. Headless CRUD | GET/POST/PUT /sws/com.etendoerp.etendorx.datasource/... | When no webhook exists for the operation, or for queries/checks |
| 3. SQL manual | Direct INSERT/UPDATE in PostgreSQL | When devassistant module is not installed or Tomcat is down |
| 4. Ask the user | Request manual action in the Etendo UI | For operations that can't be replicated via SQL (e.g., Synchronize Terminology, run triggers) |
| 5. Edit XML directly | Modify src-db/database/sourcedata/*.xml | Last resort, only with explicit user authorization |
# Check if devassistant module is installed
docker exec etendo-db-1 psql -U {bbdd.user} -d {bbdd.sid} -t -c \
"SELECT javapackage FROM ad_module WHERE javapackage = 'com.etendoerp.copilot.devassistant' AND isactive = 'Y';"
# Check if Tomcat is responding
curl -s -o /dev/null -w "%{http_code}" "${ETENDO_URL}/sws/login" 2>/dev/null
If webhooks/headless are not available, inform the user and proceed with SQL. After SQL inserts, remind the user to perform any manual steps that the webhook would have handled automatically (e.g., Synchronize Terminology, Create Columns from DB).
Editing XML files in src-db/database/sourcedata/ directly is dangerous and should only be done with explicit user authorization. The risk:
export.database will overwrite your XML editsupdate.database will overwrite DB changes that weren't exportedXML editing requires update.database afterwards (XML → DB direction), which is the reverse of the normal workflow (DB → XML via export.database).
Before editing XML: Always ask the user: "The webhooks and DB are not available. I can edit the XML files directly, but this is risky if there are unexported DB changes. Should I proceed?"
After editing XML: Always sort the file with scripts/sort_xml.py (see section 16b) to keep entries in UUID order.
Etendo stores Application Dictionary data as XML in src-db/database/sourcedata/. These files are verbose and hard to read raw. Use the bundled scripts/xml2json.py to inspect them quickly:
# List all records (compact table, audit columns hidden)
python3 scripts/xml2json.py SMFWHE_DEFINEDWEBHOOK.xml
# Show specific columns only
python3 scripts/xml2json.py SMFWHE_DEFINEDWEBHOOK.xml --cols NAME,JAVA_CLASS
# Filter by field value (partial, case-insensitive)
python3 scripts/xml2json.py AD_TABLE.xml --filter TABLENAME=smft
# Filter by record ID prefix
python3 scripts/xml2json.py AD_COLUMN.xml --id 0D9B036E
# Find records in file A missing from file B (by shared key)
python3 scripts/xml2json.py SMFWHE_DEFINEDWEBHOOK.xml \
--diff SMFWHE_DEFINEDWEBHOOK_ROLE.xml --key SMFWHE_DEFINEDWEBHOOK_ID
# Output raw JSON (for piping to other tools)
python3 scripts/xml2json.py AD_TABLE.xml --json
# Just count records
python3 scripts/xml2json.py AD_COLUMN.xml --count
The script auto-resolves filenames — pass just the filename and it searches src-db/database/sourcedata/ directories. Use this instead of grepping raw XML.
Etendo sourcedata XML files must have their entries sorted in ascending lexicographic order by UUID. When you add new entries to any XML file (e.g., new AD_MESSAGE, AD_COLUMN, or AD_REF_LIST records), sort the file immediately using the bundled scripts/sort_xml.py:
# Sort a single file after adding entries
python3 scripts/sort_xml.py src-db/database/sourcedata/AD_MESSAGE.xml
# Sort multiple files at once
python3 scripts/sort_xml.py src-db/database/sourcedata/AD_MESSAGE.xml src-db/database/sourcedata/AD_REF_LIST.xml
# Sort all sourcedata files in a module
python3 scripts/sort_xml.py path/to/module/src-db/database/sourcedata/*.xml
Output indicators:
[✓] — file was out of order and has been sorted[=] — file was already correctly sorted, no changes made[✗] — error (file not found or unrecognized format)When to run: After adding any new <AD_MESSAGE>, <AD_ELEMENT>, <AD_COLUMN>, or other sourcedata entry manually. The script detects the entity tag automatically and works for any Etendo XML format.
Do NOT skip this step — unsorted XML can cause issues when Etendo's update.database task processes the files sequentially.
After any AD creation (via webhooks or SQL fallback), validate these common pitfalls before testing in the UI:
| Check | Query | Symptom if broken |
|---|---|---|
ad_column.fieldlength > 0 | SELECT columnname FROM ad_column WHERE ad_table_id='{TID}' AND fieldlength=0 | Fields are uneditable (zero-width input) |
ad_field.displaylength not NULL/0 | SELECT name FROM ad_field WHERE ad_tab_id='{TID}' AND (displaylength IS NULL OR displaylength=0) | NullPointerException on getDisplayedLength() |
ad_table.ad_window_id linked | SELECT tablename FROM ad_table t JOIN ad_tab tb ON t.ad_table_id=tb.ad_table_id WHERE tb.ad_window_id='{WID}' AND t.ad_window_id IS NULL | FreeMarker tabView error |
ad_tab.processing = 'N' | SELECT name FROM ad_tab WHERE ad_tab_id='{TID}' AND processing IS NULL | Window rendering failures |
ad_tab.importfields = 'N' | SELECT name FROM ad_tab WHERE ad_tab_id='{TID}' AND importfields IS NULL | Window rendering failures |
See references/known-bugs-webhooks.md (B6–B9) for details on each issue and their fixes.
ALWAYS document issues encountered during a session in .etendo/skill-feedback.md in the user's project directory. This applies to any problem — not just webhooks. The file serves as a report the user can submit as an issue to improve the plugin skills.
When to write — after ANY of these events:
Write the entry as soon as the workaround is confirmed working — do not wait until the end of the session.
Format:
# Etendo Dev Assistant — Skill Feedback
## Issues
### F1: [{skill name}] — {short description}
- **What happened:** {describe the failure or unexpected behavior}
- **What was expected:** {what should have happened according to the skill}
- **How it was resolved:** {exact workaround — SQL, command, code change, etc.}
- **Affected skill:** {e.g., etendo-module, etendo-alter-db, _guidelines}
- **Suggestion:** {how the skill should be improved to prevent this}
- **Date:** {YYYY-MM-DD}
Rules:
.etendo/skill-feedback.md"ALL git operations in Etendo projects MUST follow the Etendo workflow conventions. Before any commit, branch creation, or PR, use the etendo-workflow-manager skill to validate naming and format against Git Police rules.
Never merge branches or PRs with squash. Squash merges destroy the commit history and make it impossible to trace individual changes. Always use regular merge commits to preserve the full history.
Quick reference (see workflow manager skill for full details):
| Operation | Convention |
|---|---|
| Feature branch | feature/{JIRA-KEY} |
| Hotfix branch | hotfix/#{GH}-{JIRA-KEY} |
| Feature commit | Feature {JIRA-KEY}: description (max 80 chars) |
| Hotfix commit | Issue #{GH}: description + -m "{JIRA-KEY}" |
| Co-Authored-By | Never — Git Police rejects it |
| Squash merge | Never — destroys commit history |
If a bug is detected during any /etendo:* operation, suggest creating a Jira issue using the workflow manager.
Before pushing code to a PR, run SonarQube analysis locally using /etendo:sonar to catch issues before CI. Focus on issues in new or modified files only — pre-existing issues in untouched files are not the developer's responsibility. If a SonarQube quality gate fails on a PR, use /etendo:sonar fix to identify and resolve the issues in changed files.
The official Etendo documentation at https://docs.etendo.software contains code snippets, configuration examples, API references, and how-to guides for all Etendo modules. Consult it before falling back to generic assumptions.
When to search:
How to search — use WebSearch with site filter, then WebFetch to read the page:
site:docs.etendo.software {topic}
Always look for existing code in the project before writing new code from scratch. The project already contains working examples of the same patterns you need.
find existing *EventHandler.java files in modules/ and use them as reference.BaseProcessActionHandler or DalBaseProcess.modules/*/src-db/database/sourcedata/ for existing ETRX/ETAPI records.Existing code reflects the exact versions, import paths, and patterns already proven to work in this specific project. It is the most reliable reference available.
Priority order for code examples:
modules/ directory)docs.etendo.software)references/ files (webhook details, known bugs)