Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

This page walks through creating a custom HPC application on DesignSafe. For background on what Tapis Apps are and how they work, see Tapis and Custom Apps.

Anatomy of a Tapis App

Custom Tapis Apps

On DesignSafe, you have two productive ways to run work on HPC systems.

Start with a public app if your workflow fits its interface. Write a custom app when you need non-standard flags, containers/modules, pre/post steps, or a lab-specific interface you’ll reuse.


A. Use an Existing (Public) App

Public apps on DesignSafe are vetted templates for common tools. They’re the fastest path to results.

Where to find them

How to run effectively

When public apps are ideal


B. Write Your Own App

Create a Tapis App when you need custom behavior, different software versions, or a specialized interface for your project.

Building blocks

  1. Wrapper (tapisjob_app.sh). Non-interactive, writes outputs to the job working directory, handles launch (ibrun for MPI, direct execution otherwise) and logging.

  2. app.json (required). App ID/version, runtime (ZIP for module-based apps), execution system and queue, resource defaults (nodeCount, coresPerNode, maxMinutes), file inputs, and parameters.

  3. Scheduler profile (optional). Which modules TACC preloads before your wrapper runs; most custom apps use tacc-no-modules and load their own.

  4. Tiny test dataset for validation.

Registration and sharing

Best practices

When custom apps shine


Quick Chooser

NeedPublic appYour app
Fast start with a standard toolYes
Custom binaries, flags, or launch logicYes
Team-specific interface & defaultsYes
Minimal maintenanceYes
Full control / special dependenciesYes

Creating a Custom Tapis App

This tutorial builds a small custom app end to end. The app runs a Python script on Stampede3 using TACC’s environment modules (no containers), registered with Tapis v3 and launchable from both Python and the DesignSafe portal.

The finished product is a tutorial-scale version of the production python-s3 app documented in Python App. Before building a custom app, check whether python-s3 already covers your need. Its BINARY, PRE_SCRIPT, and POST_SCRIPT settings handle many cases that once required a new app.


Prerequisites


Step 1. Set Up Your App Directory

Structure your folder like this.

my-awesome-app/
├── tapisjob_app.sh       # Wrapper Tapis executes on the compute node
├── app.json              # Tapis v3 app definition
└── test/                 # Tiny input set for validation
    ├── run_analysis.py
    └── example_input.txt

The wrapper is the only file that ships inside the app. Science scripts and data arrive at run time through the app’s Input Directory, so users change them per job without touching the app.

Step 2. Your Python Code (run_analysis.py)

Example script. This lives in the input directory, not in the app.

import sys

if len(sys.argv) != 2:
    print("Usage: python run_analysis.py <input_file>")
    sys.exit(1)

with open(sys.argv[1], 'r') as f:
    content = f.read()

print("=== File Contents ===")
print(content)
Step 3. Wrapper Script (tapisjob_app.sh)

Tapis unpacks the app archive on the compute node and executes this script from the job working directory on $SCRATCH. App arguments arrive as positional shell arguments, and each file input’s envKey becomes an environment variable holding the staged path.

#!/bin/bash
set -euo pipefail

mainScript="$1"                 # First app argument, the script filename

module load python/3.12.11      # This app manages its own modules

cd "${inputDirectory}"          # envKey of the Input Directory file input

python3 "${mainScript}" example_input.txt

Make it executable before packaging.

chmod +x tapisjob_app.sh

Exit nonzero on failure (set -e handles this). Tapis records the exit code and marks the job FAILED, which is what makes failures visible in the portal, in dapi, and to anyone debugging the job later.

Step 4. Tapis App Definition (app.json)
{
  "id": "my-awesome-app",
  "version": "1.0.0",
  "description": "Runs a Python analysis script on Stampede3",
  "runtime": "ZIP",
  "containerImage": "tapis://designsafe.storage.default/USERNAME/apps/my-awesome-app/app.zip",
  "jobType": "BATCH",
  "strictFileInputs": true,
  "jobAttributes": {
    "execSystemId": "stampede3",
    "execSystemExecDir": "${JobWorkingDir}",
    "execSystemInputDir": "${JobWorkingDir}",
    "execSystemOutputDir": "${JobWorkingDir}",
    "execSystemLogicalQueue": "skx-dev",
    "archiveSystemId": "designsafe.storage.default",
    "archiveSystemDir": "${EffectiveUserId}/tapis-jobs-archive/${JobCreateDate}/${JobName}-${JobUUID}",
    "archiveOnAppError": true,
    "nodeCount": 1,
    "coresPerNode": 1,
    "maxMinutes": 30,
    "fileInputs": [
      {
        "name": "Input Directory",
        "description": "Directory containing the main script and its input files",
        "inputMode": "REQUIRED",
        "envKey": "inputDirectory",
        "targetPath": "inputDirectory",
        "notes": { "selectionMode": "directory" }
      }
    ],
    "parameterSet": {
      "appArgs": [
        {
          "name": "Main Script",
          "description": "Filename of the Python script inside the Input Directory",
          "inputMode": "REQUIRED",
          "arg": null,
          "notes": { "inputType": "fileInput" }
        }
      ],
      "schedulerOptions": [
        {
          "name": "TACC Scheduler Profile",
          "description": "Load no modules; the wrapper loads its own",
          "inputMode": "FIXED",
          "arg": "--tapis-profile tacc-no-modules",
          "notes": { "isHidden": true }
        }
      ]
    }
  },
  "tags": ["custom", "python", "designsafe"]
}

Three fields deserve attention.

  • runtime: ZIP with a tapis:// containerImage is the standard pattern for module-based (non-container) TACC apps. See app.json Reference for every field.

  • The --tapis-profile tacc-no-modules scheduler option gives the wrapper a clean environment and full control over module loading (see Scheduler Profiles).

  • skx-dev is the right default queue while developing. Switch the default to skx once the app works.

Step 5. Package and Upload the App

Zip the wrapper. This archive is what containerImage points at.

cd my-awesome-app
zip app.zip tapisjob_app.sh

Upload it to the containerImage location. Any Tapis storage you can write works, including MyData.

from dapi import DSClient

ds = DSClient()
ds.files.upload(
    "app.zip",
    "tapis://designsafe.storage.default/USERNAME/apps/my-awesome-app/app.zip",
)

The public production apps keep their archives on a system-managed path under tapis://cloud.data/corral/tacc/aci/CEP/applications/v3/. A personal path is fine for private and shared apps.

Step 6. Register the App
import json

with open("app.json") as f:
    app_def = json.load(f)

ds.tapis.apps.createAppVersion(**app_def)

# Verify
ds.tapis.apps.getApp(appId="my-awesome-app", appVersion="1.0.0")

The same call publishes later versions. Bump version in app.json and call it again; earlier versions stay runnable, which is what keeps old jobs reproducible. For a small fix to an existing version, patch it in place.

ds.tapis.apps.patchApp(
    appId="my-awesome-app",
    appVersion="1.0.0",
    containerImage="tapis://designsafe.storage.default/USERNAME/apps/my-awesome-app/app-fixed.zip",
)
Step 7. Share the App

Apps are private to their owner until shared. Shared users can read the definition and run jobs against it.

ds.tapis.apps.shareApp(appId="my-awesome-app", users=["collaborator-username"])
Step 8. Submit a Test Job

Put the test/ folder somewhere Tapis can see it (MyData works), then use the standard dapi lifecycle.

input_uri = ds.files.to_uri("/MyData/my-awesome-app/test")

job_dict = ds.jobs.generate(
    app_id="my-awesome-app",
    input_dir_uri=input_uri,
    script_filename="run_analysis.py",
    max_minutes=10,
    allocation="YOUR_ALLOCATION",
)

submitted = ds.jobs.submit(job_dict)
final = submitted.monitor(interval=15)
ds.jobs.interpret_status(final, submitted.uuid)
Step 9. Inspect the Outputs
submitted.print_runtime_summary()
submitted.list_outputs()
print(submitted.get_output_content("tapisjob.out"))

Outputs archive to tapis-jobs-archive in MyData, the same location every DesignSafe app uses. If something failed, start with tapisjob.err and the debugging guide.

Step 10. Launch from the DesignSafe GUI

Every registered app has a portal workspace URL, even before it appears in any catalog.

https://www.designsafe-ci.org/workspace/my-awesome-app?appVersion=1.0.0
  1. Open the URL as the owner or as a user the app is shared with

  2. Fill in the Input Directory and Main Script fields, plus allocation and resources

  3. Submit. Tapis runs the job on Stampede3 and archives results to MyData

The Tools & Applications catalog itself is curated. To list an app there for the whole community, request it through the DesignSafe help desk.

Create an app with dapi

Writing app.json and a wrapper from scratch is the long road. dapi ships app templates. new() writes a working starting point in one call, and deploy() registers it under your account in a second.

ds.apps.templates()                                # ['container', 'zip']
ds.apps.new("my-app", template="zip")         # writes ./my-app/
ds.apps.deploy("./my-app")                         # registers it under your account

Choosing a template is choosing where the software stack lives.

container templatezip template
The deployed app runsany container image via apptainerany shell command on the compute node
Software stack comes fromthe image, baked in at build timeTACC modules plus whatever the wrapper sets up
Job parametersCONTAINER_IMAGE, COMMANDCOMMAND, EXTRA_MODULES
Change the softwarerebuild and push the image; the app never changesedit the wrapper, run deploy() again
Reproducibilitythe image tag or digest pins the whole stackmodule versions can move underneath you
Startup overheadimage pull and SIF conversion at job startnone
Best fortools with their own stack (gmprocess), code built off-site, exact repeatabilitycodes that live on TACC modules (OpenSees), binaries in $WORK, MPI launches via ibrun

The zip template is the skeleton to edit into a fully custom app. The container template pairs with Containers on HPC, which covers building and publishing the images it runs.

The generated files, and what to edit

new() creates a directory with two files.

FileWhat it isWhat you edit
app.jsonThe app definition Tapis registers. Everything a job form or ds.jobs.generate reads.id and version (already set from your ds.apps.new call), execSystemId and execSystemLogicalQueue for the target system and queue, default nodeCount, coresPerNode, maxMinutes, and the envVariables list, one entry per parameter your wrapper reads. Leave containerImage alone; deploy() overwrites it with the uploaded zip path.
tapisjob_app.shThe wrapper SLURM executes on the compute node. Your launch logic.The command section at the bottom. Staged inputs sit in ${_tapisExecSystemInputDir}/inputDirectory; the template cds there, so outputs written to the working directory are archived. Replace the single COMMAND line with module loads, ibrun launches, or pre/post steps as needed.

Every field is documented on The app.json File and Wrapper Scripts.

The custom-app notebook runs the whole loop, create, edit, deploy, submit, results, with a live job. Rerunning deploy() with the same version updates the registered app in place, so the edit, deploy, submit loop is fast. Bump version in app.json when a change should not overwrite what collaborators already use, and share with ds.tapis.apps.shareApp.

Tips and Best Practices