App data pipeline
Fetch and pipeline.
Source docs/en/site/sdk-appsdk-data.md
Fetch and pipeline.
Readers: people whose actions not only write the local DB, but also pull from a workspace data connection and then assemble a result. Related: App actions · App package layout · example skill pack backend/internal/skillfromchat/bundled/employee-profile/
1. When do you need a fetch pipeline?
| Scene | What to do |
|---|---|
| Only create/read/update/delete in this app’s SQLite | Built-in CRUD; no pipeline |
| One button click, fixed steps across several predefined queries, then a report | references/pipeline.json + orchestration script |
| Steps change often and should be readable by assistants/people | Put steps in the pipeline, not hardcoded in a dozen Python places |
Employee profile is the standard example: master → contacts → org → contract → pay → ID photo → assemble HTML.
2. Recommended code layout
logic/
handlers.py # only dispatch actions; write history on success/failure
generate_xxx.py # read pipeline, stepwise query_run, call assemble, save_upload
scripts/
assemble_xxx.py # pure function: data + template + mapping → HTML string
assets/
xxx.html # result layout
references/
pipeline.json # step declarations
field-mapping.json # how fields fill the template (optional)
Duty split:
- pipeline: declare “what to query, where params come from, which block to save the result as”
- generate_*: run and tolerate failure (optional steps, fallback queries, warnings)
- assemble_*: presentation; does not hit the data connection directly
- handlers: product action boundary and registration
3. Core shape of pipeline.json
{
"version": 1,
"id": "my-report-v1",
"display_name": "My report",
"input": {
"emp_name": { "type": "string", "required": true, "label": "Employee name" }
},
"steps": [
{
"id": "resolve_employee",
"label": "Resolve employee master",
"query_id": "employee_by_name",
"params": { "empName": "{{input.emp_name}}" },
"save_as": "employee",
"optional": false
},
{
"id": "fetch_org",
"label": "Org and job",
"query_id": "employee_org_by_emp_id",
"params": { "empId": "{{employee.rows[0].empId}}" },
"save_as": "org",
"optional": true
}
],
"assemble": {
"script_path": "scripts/assemble_xxx.py",
"template_path": "assets/xxx.html",
"field_mapping_path": "references/field-mapping.json"
}
}
3.1 Step fields
| Field | Meaning |
|---|---|
query_id | Predefined query id on the data connection (must already be configured) |
params | Query params; supports placeholders such as {{input.xxx}}, {{employee.rows[0].empId}} |
save_as | Name of this step’s result in later bindings |
optional | true: failure or empty result goes into warnings; the whole job does not fail |
fallback_step | Backup query_id + params when the main query has no data (for example ID photo) |
In-app orchestration scripts read these fields; keys such as
tool/actionmatter more when skills run from conversation. Inside the app you usually callplatform.query_rundirectly.
3.2 Relation to data connections
- The workspace must already have a data connection, and query ids must match the pipeline.
- The user must be allowed to run those queries.
- You can
platform.query_list()first to see which ids exist, to avoid a hard failure.
4. What the orchestration script does (logic skeleton)
Pseudocode (same family as employee-profile generate_profile.py):
run_generate(params, ctx):
validate required input
platform = ctx["platform"]
pipeline = read references/pipeline.json
bindings = { input: params }
warnings = []
for step in pipeline.steps:
resolve params template
block = platform.query_run(query_id, params)
if empty and fallback exists → query again
if still fail and optional → warnings.append(...); block = empty
if fail and not optional → return error
bindings[save_as] = block
html = assemble(bindings, template, field_mapping)
up = platform.save_upload(filename, html, "text/html; charset=utf-8")
return { ok, upload_id, warnings, ... }
Disambiguation: when the master has several rows, return code: "disambiguate" + candidates, so the user can fill an employee ID and submit again (the UI already supports showing this kind of error).
5. Assemble and field mapping
- Template:
assets/*.html; placeholders or script replacement. - field-mapping.json: describes “this report column comes from which query field”, empty values, masking, and similar (employee profile has a full sample).
- Result page from a picture: see App views §4 path A.
Assemble scripts should avoid platform-bridge calls so they are easy to unit-test and reuse on the skill side.
6. How handlers attach the pipeline
from generate_xxx import run_generate
def handle(action, params, ctx):
if action == "report.generate":
result = run_generate(params, ctx)
# Optional: write status / upload_id into the app.db history table
return result
...
UI: action_form → report.generate; history: list + entity with upload_id/status.
7. Must-have checklist (fetch-style apps)
- [ ] Workspace data connection works; every non-optional
query_idin the pipeline exists - [ ]
logic/handlers.pyaction ids matchapp.json - [ ] Orchestration script uses
ctx["platform"]; no forbidden network libraries - [ ]
references/pipeline.json(or equivalent steps in the script) matches real query param names - [ ] When preview is needed:
save_upload+ returnupload_id - [ ] Partial failure returns
warnings; do not silently drop fields
8. App pack panel (browse and allowlisted edit)
After you open the app, “App pack” can show the in-pack file tree and preview contents.
| Capability | Notes |
|---|---|
| Browse | app.json, manifest.json, and text files under logic/ scripts/ references/ assets/ skills/ |
| Edit and save | Allowlisted paths only (same write scope as the app-development assistant, for example logic/*.py, scripts/*.py, references/*.{json,md}, assets/profile.html, skills/SKILL.md) |
| Read-only | app.json / manifest.json and similar: change UI description via the app-development assistant or against the spec separately |
HTTP (members):
GET .../apps/{appId}/package/filesGET .../apps/{appId}/package/file?rel_path=PUT .../apps/{appId}/package/filebody{ rel_path, content }
9. Do not put these in the pipeline
| Content | Belongs in |
|---|---|
| Button copy, list columns | app.json |
| Permission model | Data-connection ACL / workspace members |
| Arbitrary SQL | Predefined queries (admin or conversation write); scripts only query_run |
| Pixel-level operations-console UI | Platform plugin, not pipeline |