skills/dnanexus-integration/references/app-development.md
main is used for a
normal app/applet run; other entry points can be launched as subjobs.Develop as an applet, test in a non-production project, then create and publish an app only after reviewing code, permissions, dependencies, and regions.
my-app/
├── dxapp.json
├── src/
│ └── my_app.py
├── resources/
│ └── requirements.txt
└── test/
├── input.json
└── expected.json
resources/ is bundled into the executable. Never place tokens, private keys,
registry passwords, or patient data in it.
dx-app-wizard
The wizard supports templates such as:
basicparallelizedscatter-process-gatherThe generated code is a starting point, not a production security boundary. Review all access and dependency fields.
from __future__ import annotations
import subprocess
from pathlib import Path
from typing import Any
import dxpy
@dxpy.entry_point("main")
def main(reads: dict[str, Any], min_quality: int = 20) -> dict[str, Any]:
if not 0 <= min_quality <= 93:
raise dxpy.AppError("min_quality must be between 0 and 93")
input_path = Path("reads.fastq.gz")
output_path = Path("filtered.fastq.gz")
reads_file = dxpy.get_handler(reads)
if not isinstance(reads_file, dxpy.DXFile):
raise dxpy.AppError("reads must reference a DNAnexus file")
dxpy.download_dxfile(reads_file, str(input_path))
# Fixed executable + argv list; no shell interpretation of user input.
subprocess.run(
[
"quality-filter",
"--input",
str(input_path),
"--output",
str(output_path),
"--min-quality",
str(min_quality),
],
check=True,
)
report = dxpy.upload_local_file(
str(output_path),
wait_on_close=True,
)
return {"filtered_reads": dxpy.dxlink(report.get_id())}
dxpy.run()
Key points:
dxpy.get_handler() accepts an ID or DNAnexus link.shell=True with inputs.outputSpec.AppError for an expected, actionable user/input error.The Python execution template reads job_input.json, calls the selected entry
point with keyword arguments, and writes the returned mapping to
job_output.json.
#!/usr/bin/env bash
main() {
dx download "$reads" --output "reads.fastq.gz"
quality-filter \
--input "reads.fastq.gz" \
--output "filtered.fastq.gz" \
--min-quality "$min_quality"
filtered_reads="$(dx upload "filtered.fastq.gz" --brief)"
dx-jobutil-add-output \
"filtered_reads" "$filtered_reads" --class=file
}
The platform invokes Bash with error-exit behavior. Still quote variables, validate scalar inputs, and avoid building command strings. Input values are untrusted even when provided by a trusted platform user.
Current AEEs:
00Jobs run on ephemeral workers. The platform:
execDepends.stdout and stderr.job_output.json or job_error.json.Useful system-provided values include:
DX_JOB_IDDX_WORKSPACE_IDDX_PROJECT_CONTEXT_IDDX_RESOURCES_IDDX_SECURITY_CONTEXTUse dxpy rather than parsing these directly where possible. Never print the
security context.
Network access is restricted unless requested in app metadata. Prefer a domain
allowlist; do not request ["*"] solely to install dependencies at runtime.
Do not use python src/my_app.py as the only local test. dxpy.run() expects
the platform execution contract.
Instead:
Example:
def build_command(
input_path: str,
output_path: str,
min_quality: int,
) -> list[str]:
if not 0 <= min_quality <= 93:
raise ValueError("min_quality must be between 0 and 93")
return [
"quality-filter",
"--input",
input_path,
"--output",
output_path,
"--min-quality",
str(min_quality),
]
Test build_command() locally without credentials.
Validate offline from this skill's root (or resolve scripts/ relative to the
loaded skill directory):
uv run python "scripts/validate_dxapp.py" \
"/path/to/my-app/dxapp.json" --kind applet --strict
Build an applet:
dx build "my-app"
Build a versioned app:
dx build "my-app" --create-app
Use dx build --help from the installed toolkit for destination, overwrite,
regional, and advanced flags; these evolve with dx-toolkit.
After build:
dx describe "applet-xxxx"
dx run "applet-xxxx" -h
Verify:
Use a dedicated test project and explicit destination:
dx run "applet-xxxx" \
--input-json-file "test/input.json" \
--destination "project-test:/test-runs/run-001"
Keep interactive confirmation. Set a small cost limit for automation:
dx run "applet-xxxx" \
--input-json-file "test/input.json" \
--destination "project-test:/test-runs/run-002" \
--cost-limit 5
Monitor:
dx watch "job-xxxx"
dx describe "job-xxxx"
Test:
Only use subjobs for independent work large enough to justify worker startup.
import dxpy
@dxpy.entry_point("main")
def main(items):
jobs = [
dxpy.new_dxjob(
fn_input={"item": item},
fn_name="process_item",
)
for item in items
]
return {
"results": [
job.get_output_ref("result")
for job in jobs
]
}
@dxpy.entry_point("process_item")
def process_item(item):
result = process_one(item)
return {"result": result}
dxpy.run()
Do not wait synchronously for child jobs when output references can express the
dependency. Ensure every restartable entry point is idempotent before setting
restartableEntryPoints to all.
DNAnexus can reuse identical completed jobs. Preserve reuse for deterministic workloads to save time and cost.
Disable reuse only when:
If an app writes outside its output folder, uses the current time, downloads floating resources, or mutates a shared record, document that behavior and reconsider whether it is suitable for reuse/restarts.
Use failure categories deliberately:
AppError: expected user/data problem with an actionable messageAppInternalError: unexpected application defect or nonzero process exitAppInsufficientResourceError: insufficient memory/storage conditionInputError / OutputError: platform contract mismatchDXExecDependencyError: dependency installation failureDo not mark partial scientific output as success. If safe partial results are valuable, publish them under explicitly named diagnostic outputs and still return the appropriate failure.
openSource) chosen intentionally