# GitHub Actions–inspired features for jan
This page is a **design notebook**, not the shipped interface. Typed `inputs:`, `packages.*`, language `exec` forms, and `tests:` have landed; see the YAML spec section of this site for current behavior. Remaining items (`if:`, per-node `workdir`, secrets other than `env.pass`) are proposals.
Jan today is a **nested command tree** (`commands` → `exec`) with local runtime metadata (`env`, `requires`, `dependencies`, `os`, `cron`, `include`). GitHub Actions is a **workflow graph** (`on` → `jobs` → `steps`). The useful inspiration is the *declarative vocabulary*, not cloning Actions’ job graph wholesale.
```mermaid
flowchart LR
subgraph gha [GitHub Actions]
on --> jobs --> steps
end
subgraph jan [jan today]
commands --> exec
cron --> runLeaf[run leaf]
end
## Already close to Actions
| `name:` / description | `metadata`, `about` |
| `runs-on:` / OS | `os:` filter |
| `env:` | `env:` (chain-merged) |
| `defaults.run.working-directory` | global `--cwd` only (no per-node `workdir`) |
| reusable workflows | `include:` (root list + per-node) |
| schedule trigger | `cron:` + `jan cron` |
| “needs another checkout’s tools” | `dependencies:` / `path:` / `requires:` |
| composite action packing | `jan bundle` / install |
## High-value features (fit jan’s model)
These extend `CommandNode` / `ExecSpec` without turning leaves into multi-step workflows.
### 1. Typed inputs (`inputs:` + expressions)
**Shipped.** Declare CLI args on a script node; interpolate into `env` / `argv`.
```yaml
backup:
about: Backup a path
inputs:
path:
description: Source directory
required: true
dest:
default: ~/Backups
env:
public:
SRC: ${{ inputs.path }}
commands:
run:
exec:
argv: ["bash", "-lc", "rsync -a \"$SRC\" \"${{ inputs.dest }}\""]
```
```bash
jan … backup run --path ~/Documents
jan … backup run --path ~/Documents --dest /mnt/backup
```
Only `${{ inputs.<name> }}` expressions are supported (not a full Actions expression language).
### 2. Conditionals (`if:`)
Actions: `if: runner.os == 'Linux' && inputs.mode == 'full'`.
**jan shape:** skip or hide a node / refuse run when expression is false. Start small: `os`, env vars, inputs, exit of a tiny predicate—not a full JS-like language.
```yaml
ports:
if: ${{ os == 'linux' }}
exec:
argv: ["ss", "-tlnp"]
```
(`os:` already hard-filters; `if:` generalizes to runtime/env/input conditions.)
### 3. Per-node working directory + shell defaults
Actions: `defaults.run`, `working-directory`, `shell:`.
```yaml
exec:
argv: ["pytest"]
workdir: ${{ inputs.path }}
shell: bash # optional sugar over bash -lc
```
Today only global `--cwd`. Per-leaf `workdir` is a small ExecSpec change with big ergonomics for script trees.
### 4. Timeout and failure policy
Actions: `timeout-minutes`, `continue-on-error`.
```yaml
run:
timeout: 5m
continue-on-error: true
exec:
argv: ["bash", "-lc", "..."]
```
Especially useful for `jan cron` (one hung script should not block the minute).
### 5. Richer triggers (still local)
Actions: `on: schedule | workflow_dispatch | push | …`.
Keep triggers **local and explicit**—do not watch git remotes inside jan:
| `schedule` | done (`cron`) |
| `workflow_dispatch` | normal `jan … run` (+ inputs) |
| path/file hooks | optional `watch:` / `jan watch` (inotify) — stretch |
| “run after X” | `needs:` among cron jobs or a tiny `pipeline:` — stretch |
A GHA-flavored umbrella field is optional sugar, not required:
```yaml
on:
schedule: ["30 10 * * *"]
# dispatch is implicit via CLI
```
### 6. Secrets / env files (local analogue of `secrets:`)
**Shipped:** `env.private` lists names that must be set in the host environment; values are copied into the child and never stored in YAML. `env.public` holds assignable values. When either is set, the child gets a cleared environment (essentials + listed vars only).
```yaml
env:
public:
ISSUE_EDITOR: vim
private:
- GH_TOKEN
```
Future sugar may still add `${{ secret.NAME }}` expression forms; existence checks already cover the main security need.
## Medium-value / stretch
### 7. Multi-step `run:` sequences (mini-jobs)
```yaml
deploy:
steps:
- run: jan scripts misc preflight run
- run: ./deploy.sh
workdir: infra
- if: ${{ success() }}
run: jan notify run
```
This is the biggest semantic shift: a leaf becomes a **job**. Worth it only if many scripts are glue of several execs; otherwise keep one `exec` and let bash/Justfile own sequences.
### 8. `needs:` / concurrency for cron
```yaml
nightly:
cron: "0 2 * * *"
needs: [backup]
concurrency: { group: nightly, cancel-in-progress: true }
```
Useful once many cron scripts exist; needs a small lockfile under XDG runtime dir.
### 9. Matrix
```yaml
matrix:
python: ["3.11", "3.12"]
strategy:
fail-fast: false
```
Powerful for CI; usually overkill for personal scripts. Defer unless you routinely fan out the same leaf.
### 10. Step outputs / `$GITHUB_OUTPUT` analogue
```yaml
steps:
- id: ver
run: echo "v=$(git describe)" >> "$JAN_OUTPUT"
- run: echo "built ${{ steps.ver.outputs.v }}"
```
Only pays off with multi-step jobs (#7).
### 11. Reusable “actions” (`uses:`)
Beyond `include:` (structural merge), `uses: ./actions/notify.yaml` with inputs/outputs would mirror composite actions. Natural evolution of includes + inputs.
## Poor fits (skip or keep out of core)
- **Hosted runners / `runs-on: ubuntu-latest`** — jan already has host `os:`; no remote fleet.
- **`permissions:` / OIDC / GitHub token scopes** — use OS user + secret refs.
- **Marketplace `uses: actions/checkout@v4`** — out of scope; local `uses:` paths only.
- **PR/push event metadata** — unless you add an explicit `jan hook git …` that *receives* payload from a user-installed hook; don’t poll remotes.
- **Full `${{ }}` expression language** — start with a tiny evaluator (literals, inputs, env, os, `success()`); avoid JS parity.
## Design principle
Stay a **CLI tree with declarative run metadata**, not a second Actions runner:
1. Prefer fields on existing nodes (`inputs`, `if`, `timeout`, `workdir`).
2. Add new builtins only when host orchestration is needed (`jan cron` pattern → maybe `jan watch`, `jan pipeline`).
3. Keep Expressions minimal and validated by `jan validate`.
4. Introduce `steps:` only when single-`exec` + shell becomes clearly painful.
## Suggested adoption order
If/when implementing (not part of this exploration’s deliverable):
1. **`inputs:` + interpolation** — shipped
2. **`workdir` / `timeout` / `continue-on-error` on `exec`** — small, cron-safe
3. **`if:`** — generalizes `os:`
4. **secret refs** — `env.private` covers existence; expression sugar still optional
5. **`steps:` + outputs** — only if needed
6. **`needs:` / concurrency / watch / matrix** — last
## Illustrative “Actions-flavored” leaf (target dialect)
```yaml
sync-notes:
about: Sync notes vault
cron: "@daily"
inputs:
vault:
default: ~/Notes
if: ${{ env.JAN_ENABLE_NOTES == '1' }}
requires: [rsync]
env:
VAULT: ${{ inputs.vault }}
commands:
run:
timeout: 10m
continue-on-error: false
exec:
workdir: ${{ inputs.vault }}
argv: ["bash", "-lc", "rsync -a ./ remote:notes/"]
```
No code changes in this plan—this is the feature map to pull from when extending jan’s YAML.