Skill Tracer
Skills › Scientific Agent Skills › dnanexus-integration › app-development.md

app-development.md

375 lines · skills/dnanexus-integration/references/app-development.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# App and Applet Development
2

App and Applet Development

Raw lines 1 to 2
3## Quick Navigation
4
5- [Model and source layout](#model)
6- [Python and Bash entry points](#python-entry-point)
7- [Execution environment](#execution-environment)
8- [Local testing](#separate-pure-logic-from-platform-io)
9- [Build and platform tests](#build)
10- [Subjobs, reuse, and errors](#subjobs-and-parallelism)
11- [Publish checklist](#publish-checklist)

Quick Navigation

  • Model and source layout
  • Python and Bash entry points
  • Execution environment
  • Local testing
  • Build and platform tests
  • Subjobs, reuse, and errors
  • Publish checklist
Raw lines 3 to 12
13## Model
15- **Applet**: immutable executable data object in one project; best for
16 development, testing, and project-local tools.
17- **App**: versioned executable that can be authorized, published, and run
18 across projects/regions; best for maintained reusable products.
19- **Job**: execution of an app or applet.
20- **Entry point**: named function within an executable. `main` is used for a
21 normal app/applet run; other entry points can be launched as subjobs.
23Develop as an applet, test in a non-production project, then create and publish
24an app only after reviewing code, permissions, dependencies, and regions.

Model

  • Applet: immutable executable data object in one project; best for development, testing, and project-local tools.
  • App: versioned executable that can be authorized, published, and run across projects/regions; best for maintained reusable products.
  • Job: execution of an app or applet.
  • Entry point: named function within an executable. 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.

Raw lines 13 to 25
26## Source Layout
28```text
29my-app/
30├── dxapp.json
31├── src/
32│ └── my_app.py
33├── resources/
34│ └── requirements.txt
35└── test/
36 ├── input.json
37 └── expected.json
38```
40`resources/` is bundled into the executable. Never place tokens, private keys,
41registry passwords, or patient data in it.

Source Layout

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.

Raw lines 26 to 42
43## Create a Skeleton
45```bash
46dx-app-wizard
47```
49The wizard supports templates such as:
51- `basic`
52- `parallelized`
53- `scatter-process-gather`
55The generated code is a starting point, not a production security boundary.
56Review all access and dependency fields.

Create a Skeleton

dx-app-wizard

The wizard supports templates such as:

  • basic
  • parallelized
  • scatter-process-gather

The generated code is a starting point, not a production security boundary. Review all access and dependency fields.

Raw lines 43 to 57
58## Python Entry Point
60The following adapter is illustrative: `quality-filter` is a placeholder for
61your tested scientific tool, not a bundled executable. It has not been run on
62a DNAnexus worker during this documentation review.
64```python
65from __future__ import annotations
67import subprocess
68from pathlib import Path
69from typing import Any
71import dxpy
74@dxpy.entry_point("main")
75def main(reads: dict[str, Any], min_quality: int = 20) -> dict[str, Any]:
76 if not 0 <= min_quality <= 93:
77 raise dxpy.AppError("min_quality must be between 0 and 93")
79 input_path = Path("reads.fastq.gz")
80 output_path = Path("filtered.fastq.gz")
82 reads_file = dxpy.get_handler(reads)
83 if not isinstance(reads_file, dxpy.DXFile):
84 raise dxpy.AppError("reads must reference a DNAnexus file")
86 dxpy.download_dxfile(reads_file, str(input_path))
88 # Fixed executable + argv list; no shell interpretation of user input.
89 subprocess.run(
90 [
91 "quality-filter",
92 "--input",
93 str(input_path),
94 "--output",
95 str(output_path),
96 "--min-quality",
97 str(min_quality),
98 ],
99 check=True,
100 )
102 report = dxpy.upload_local_file(
103 str(output_path),
104 wait_on_close=True,
105 )
106 return {"filtered_reads": dxpy.dxlink(report.get_id())}
109dxpy.run()
110```
112Key points:
114- `dxpy.get_handler()` accepts an ID or DNAnexus link.
115- Pass subprocess arguments as a list. Do not use `shell=True` with inputs.
116- Return a mapping whose keys exactly match `outputSpec`.
117- Return file/record outputs as DNAnexus links.
118- Use `AppError` for an expected, actionable user/input error.
119- Do not catch every exception and convert it to success.
121The Python execution template reads `job_input.json`, calls the selected entry
122point with keyword arguments, and writes the returned mapping to
123`job_output.json`.

Python Entry Point

The following adapter is illustrative: quality-filter is a placeholder for your tested scientific tool, not a bundled executable. It has not been run on a DNAnexus worker during this documentation review.

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.
  • Pass subprocess arguments as a list. Do not use shell=True with inputs.
  • Return a mapping whose keys exactly match outputSpec.
  • Return file/record outputs as DNAnexus links.
  • Use AppError for an expected, actionable user/input error.
  • Do not catch every exception and convert it to success.

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.

Raw lines 58 to 124
125## Bash Entry Point
127```bash
128#!/usr/bin/env bash
130main() {
131 dx download "$reads" --output "reads.fastq.gz"
133 quality-filter \
134 --input "reads.fastq.gz" \
135 --output "filtered.fastq.gz" \
136 --min-quality "$min_quality"
138 filtered_reads="$(dx upload "filtered.fastq.gz" --brief)"
139 dx-jobutil-add-output \
140 "filtered_reads" "$filtered_reads" --class=file
142```
144The platform invokes Bash with error-exit behavior. Still quote variables,
145validate scalar inputs, and avoid building command strings. Input values are
146untrusted even when provided by a trusted platform user.

Bash Entry Point

#!/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.

Raw lines 125 to 147
Runs code bin/env :128
148## Execution Environment
150Current AEEs:
152- Ubuntu 24.04, version `0`
153- Ubuntu 20.04, version `0`
155Jobs run on ephemeral workers. The platform:
1571. Provisions the worker and app container.
1582. Installs `execDepends`.
1593. Configures API, networking, and logs.
1604. Unpacks bundled dependencies and assets.
1615. Runs the selected interpreter/entry point.
1626. Captures `stdout` and `stderr`.
1637. Processes `job_output.json` or `job_error.json`.
1648. Destroys the workspace unless a supported debugging hold is requested.
166Useful system-provided values include:
168- `DX_JOB_ID`
169- `DX_WORKSPACE_ID`
170- `DX_PROJECT_CONTEXT_ID`
171- `DX_RESOURCES_ID`
172- `DX_SECURITY_CONTEXT`
174Use `dxpy` rather than parsing these directly where possible. Never print the
175security context.
177Network access is restricted unless requested in app metadata. Prefer a domain
178allowlist; do not request `["*"]` solely to install dependencies at runtime.

Execution Environment

Current AEEs:

  • Ubuntu 24.04, version 0
  • Ubuntu 20.04, version 0

Jobs run on ephemeral workers. The platform:

  1. Provisions the worker and app container.
  2. Installs execDepends.
  3. Configures API, networking, and logs.
  4. Unpacks bundled dependencies and assets.
  5. Runs the selected interpreter/entry point.
  6. Captures stdout and stderr.
  7. Processes job_output.json or job_error.json.
  8. Destroys the workspace unless a supported debugging hold is requested.

Useful system-provided values include:

  • DX_JOB_ID
  • DX_WORKSPACE_ID
  • DX_PROJECT_CONTEXT_ID
  • DX_RESOURCES_ID
  • DX_SECURITY_CONTEXT

Use 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.

Raw lines 148 to 179
180## Separate Pure Logic from Platform I/O
182Do not use `python src/my_app.py` as the only local test. `dxpy.run()` expects
183the platform execution contract.
185Instead:
1871. Put scientific logic in ordinary functions/modules.
1882. Unit-test those functions with local files.
1893. Keep the entry point as a thin download → validate → compute → upload
190 adapter.
1914. Test the packaged applet on DNAnexus with small non-sensitive fixtures.
193Example:
195```python
196def build_command(
197 input_path: str,
198 output_path: str,
199 min_quality: int,
200) -> list[str]:
201 if not 0 <= min_quality <= 93:
202 raise ValueError("min_quality must be between 0 and 93")
203 return [
204 "quality-filter",
205 "--input",
206 input_path,
207 "--output",
208 output_path,
209 "--min-quality",
210 str(min_quality),
211 ]
212```
214Test `build_command()` locally without credentials.

Separate Pure Logic from Platform I/O

Do not use python src/my_app.py as the only local test. dxpy.run() expects the platform execution contract.

Instead:

  1. Put scientific logic in ordinary functions/modules.
  2. Unit-test those functions with local files.
  3. Keep the entry point as a thin download → validate → compute → upload adapter.
  4. Test the packaged applet on DNAnexus with small non-sensitive fixtures.

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.

Raw lines 180 to 215
Runs code python src/my_app.py :182
216## Build
218Validate offline from this skill's root (or resolve `scripts/` relative to the
219loaded skill directory):
221```bash
222uv run python "scripts/validate_dxapp.py" \
223 "/path/to/my-app/dxapp.json" --kind applet --strict
224```
226Build an applet:
228```bash
229dx build "my-app"
230```
232Build a versioned app:
234```bash
235dx build "my-app" --create-app
236```
238Use `dx build --help` from the installed toolkit for destination, overwrite,
239regional, and advanced flags; these evolve with dx-toolkit.
241After build:
243```bash
244dx describe "applet-xxxx"
245dx run "applet-xxxx" -h
246```
248Verify:
250- Input/output fields and defaults
251- AEE release
252- Regions and instance requirements
253- Access and network requirements
254- Dependency records/files
255- Timeout and restart policy

Build

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:

  • Input/output fields and defaults
  • AEE release
  • Regions and instance requirements
  • Access and network requirements
  • Dependency records/files
  • Timeout and restart policy
Raw lines 216 to 256
Runs code scripts/validate_dxapp.py :222
257## Test on Platform
259Use a dedicated test project and explicit destination:
261```bash
262dx run "applet-xxxx" \
263 --input-json-file "test/input.json" \
264 --destination "project-test:/test-runs/run-001"
265```
267Keep interactive confirmation. Set a small cost limit for automation:
269```bash
270dx run "applet-xxxx" \
271 --input-json-file "test/input.json" \
272 --destination "project-test:/test-runs/run-002" \
273 --cost-limit 5
274```
276Monitor:
278```bash
279dx watch "job-xxxx"
280dx describe "job-xxxx"
281```
283Test:
285- Required and optional inputs
286- Malformed and incompatible inputs
287- Empty and boundary-size data
288- Output classes, names, and closed states
289- Retry behavior and idempotency
290- Network-denied behavior
291- Resource exhaustion behavior
292- Duplicate execution and reuse

Test on Platform

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:

  • Required and optional inputs
  • Malformed and incompatible inputs
  • Empty and boundary-size data
  • Output classes, names, and closed states
  • Retry behavior and idempotency
  • Network-denied behavior
  • Resource exhaustion behavior
  • Duplicate execution and reuse
Raw lines 257 to 293
294## Subjobs and Parallelism
296Only use subjobs for independent work large enough to justify worker startup.
298```python
299import dxpy
302@dxpy.entry_point("main")
303def main(items):
304 jobs = [
305 dxpy.new_dxjob(
306 fn_input={"item": item},
307 fn_name="process_item",
308 )
309 for item in items
310 ]
311 return {
312 "results": [
313 job.get_output_ref("result")
314 for job in jobs
315 ]
316 }
319@dxpy.entry_point("process_item")
320def process_item(item):
321 result = process_one(item)
322 return {"result": result}
325dxpy.run()
326```
328Do not wait synchronously for child jobs when output references can express the
329dependency. Ensure every restartable entry point is idempotent before setting
330`restartableEntryPoints` to `all`.

Subjobs and Parallelism

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.

Raw lines 294 to 331
332## Reuse and Idempotency
334DNAnexus can reuse identical completed jobs. Preserve reuse for deterministic
335workloads to save time and cost.
337Disable reuse only when:
339- The executable intentionally depends on untracked external state.
340- A debugging run must execute again.
341- The user explicitly requires recomputation.
343If an app writes outside its output folder, uses the current time, downloads
344floating resources, or mutates a shared record, document that behavior and
345reconsider whether it is suitable for reuse/restarts.

Reuse and Idempotency

DNAnexus can reuse identical completed jobs. Preserve reuse for deterministic workloads to save time and cost.

Disable reuse only when:

  • The executable intentionally depends on untracked external state.
  • A debugging run must execute again.
  • The user explicitly requires recomputation.

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.

Raw lines 332 to 346
347## Errors
349Use failure categories deliberately:
351- `AppError`: expected user/data problem with an actionable message
352- `AppInternalError`: unexpected application defect or nonzero process exit
353- `AppInsufficientResourceError`: insufficient memory/storage condition
354- `InputError` / `OutputError`: platform contract mismatch
355- `DXExecDependencyError`: dependency installation failure
357Do not mark partial scientific output as success. If safe partial results are
358valuable, publish them under explicitly named diagnostic outputs and still
359return the appropriate failure.

Errors

Use failure categories deliberately:

  • AppError: expected user/data problem with an actionable message
  • AppInternalError: unexpected application defect or nonzero process exit
  • AppInsufficientResourceError: insufficient memory/storage condition
  • InputError / OutputError: platform contract mismatch
  • DXExecDependencyError: dependency installation failure

Do 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.

Raw lines 347 to 360
361## Publish Checklist
363- Source and dependency licenses reviewed
364- App version incremented
365- Inputs/outputs backward compatible or breaking change documented
366- Reproducible assets/images pinned
367- No embedded credentials or sensitive fixtures
368- Network and project access minimized
369- Supported regions tested
370- Instance types currently available
371- Timeouts, retries, and cost behavior tested
372- Output metadata and scientific provenance included
373- App source visibility (`openSource`) chosen intentionally
374- Authorized users/orgs reviewed
375- Applet test evidence retained

Publish Checklist

  • Source and dependency licenses reviewed
  • App version incremented
  • Inputs/outputs backward compatible or breaking change documented
  • Reproducible assets/images pinned
  • No embedded credentials or sensitive fixtures
  • Network and project access minimized
  • Supported regions tested
  • Instance types currently available
  • Timeouts, retries, and cost behavior tested
  • Output metadata and scientific provenance included
  • App source visibility (openSource) chosen intentionally
  • Authorized users/orgs reviewed
  • Applet test evidence retained
Raw lines 361 to 375