# IML Workflow Reference **Workflows-as-Code for InTouch.** An IML workflow (a "Workflow File") is a single IML file that declares a runnable automation: an ordered list of tasks, optional alert notifications, and optional bundled content. Install it and the workflow is runnable. **Automation (schedules, trigger files) is NOT part of the IML** — it is attached separately after install (see [Automation is attached separately](#automation-is-attached-separately)). Contents: 1. [Minimal template](#minimal-template) 2. [Maximal template](#maximal-template) 3. [Top-level fields](#top-level-fields) 4. [tasks](#tasks) 5. [notifications (subscriptions)](#notifications-subscriptions) 6. [content](#content) 7. [Variable interpolation](#variable-interpolation) 8. [Where the file lives](#where-the-file-lives) 9. [Automation is attached separately](#automation-is-attached-separately) 10. [Install behavior](#install-behavior) 11. [Validation rules](#validation-rules) 12. [Hub publish conventions](#hub-publish-conventions) 13. [Idiom patterns](#idiom-patterns) --- ## Minimal template ```json { "name": "hello-world", "version": "1.0.0", "description": "The smallest valid IML workflow.", "tasks": [ { "name": "say-hello", "tool": "runtimeenv", "properties": { "runtimeEnvName": "bash", "scriptContent": "echo \"Hello, InTouch.\"\n" } } ] } ``` Installing this writes the file to disk and registers a runnable workflow with one task and nothing else — no schedule, no alerts, no trigger. Run it via the UI, on-demand, or `POST /intouch/iml-workflow/run`. ## Maximal template Every legal field, annotated. There are NO other top-level fields — anything else is silently ignored by the parser (`FAIL_ON_UNKNOWN_PROPERTIES` is off), so a misspelled or invented key just disappears. Stick to exactly these. ```json { "name": "appointment-reminder", "version": "1.2.0", "description": "Sends an SMS reminder before each appointment.\n", "category": "Customer Service", "tasks": [ { "name": "load-appointments", "tool": "sql", "credential": "appointments-db", "operation": "export", "properties": { "statement": "SELECT customer_phone, appt_time FROM appointments WHERE appt_time < now() + interval '25 hours'", "outputFile": "/tmp/appointments.csv" }, "maxTime": 30 }, { "name": "send-reminder", "tool": "sms", "credential": "twilio-prod", "properties": { "to": "{{load-appointments.customer_phone}}", "message": "Reminder: your appointment is at {{load-appointments.appt_time}}" }, "maxTime": 120 } ], "notifications": [ { "contactNames": [ "oncall" ], "alertOnError": true, "alertOnDone": false, "sendDetail": true } ], "content": [ { "type": "url", "uri": "https://docs.example.com/appointments-runbook" }, { "type": "text", "uri": "Oncall: see runbook if SMS failures exceed 5%." } ] } ``` ## Top-level fields These are the **only** fields the parser reads (`ImlWorkflowDefinition`): | Field | Type | Required | Default | Notes | |---|---|:---:|---|---| | `name` | string | ✅ | — | Unique. Lowercase kebab-case. Becomes the workflow's `bis_workflow.name` (the on-disk folder is keyed by the workflow's id, not this name). | | `version` | string | — (✅ for hub) | `"1.0.0"` | Semver string. The hub rejects publishes without a version bump. | | `description` | string | — | `""` | Shown in the UI and browse catalog. | | `category` | string | — | `""` | Display-only tag (e.g. "Family & Home"). NOT an area/track name. | | `tasks` | list | ✅ | `[]` | The task list. Runs strictly in declaration order. See [tasks](#tasks). | | `notifications` | list | — | `[]` | Alert delivery rules. Alias: `subscriptions`. See [notifications](#notifications-subscriptions). | | `content` | list | — | `[]` | Attachments/text/URLs. See [content](#content). | **There is NO `schedule`, `triggerFile`, `cron`, `enabled`, `maxRetries`, or `retryIntervalSeconds` field.** Those do not exist in the IML workflow format. Scheduling and triggers are attached to the workflow *after* install — see [Automation is attached separately](#automation-is-attached-separately). ## tasks Tasks **execute strictly in list order, one at a time.** There is no dependency graph, no parallelism, no branching, and no `after:` / `onFailure:` field — **the order is the order.** Any task failure stops the workflow. | Field | Type | Required | Default | Notes | |---|---|:---:|---|---| | `name` | string | ✅ | — | Unique within the workflow. This is the reference key for `{{name.field}}` interpolation. No dots in the name. | | `tool` | string | ✅ | — | Tool name registered in `TaskConnectorRegistry` (built-in like `sql`/`http`/`runtimeenv`, JAR plugin, or IML tool). Validated at install. **The field is `tool:` — never `type:`.** | | `credential` | string | if the tool needs one | `""` | Alias: `connection`. Name of a credential on the server. | | `operation` | string | tool-dependent | `""` | Sub-operation (e.g. `sql`: `export`/`import`/`statement`). | | `properties` | map | — | `{}` | Tool-specific input. Tool-specific keys go HERE, never at the task root. | | `maxTime` | int | — | `60` | Per-task timeout in **seconds**. `0` = engine default (3600s). | ## notifications (subscriptions) Sends alerts through each contact's configured transports (inbox / email / SMS / Slack / Discord / Telegram / WhatsApp / Teams / LINE) when the workflow hits an alert condition. The top-level key may be written `notifications:` or `subscriptions:` — both work. ```json { "notifications": [ { "contactNames": [ "oncall" ], "userNames": [ "intouch" ], "alertOnStart": false, "alertOnDone": false, "alertOnError": true, "alertOnOverrun": false, "alertOnRetry": false, "alertOnWarning": false, "sendContent": false, "sendDetail": false } ] } ``` The inner field is `contactNames` (a list; `contacts` is an accepted alias) — **not** the singular `contact:`. A single bare name is accepted and treated as a one-element list. On a fresh install the only delivery path that always works is the inbox via `userNames: [intouch]`; any external channel needs the transport configured in Server Settings plus a contact with a profile on that channel. ## content Static content bundled with the workflow — attached to alert notifications when a notification sets `sendContent: true`. ```json { "content": [ { "type": "attachment", "uri": "/srv/templates/invoice.pdf" }, { "type": "text", "uri": "Oncall runbook: https://example.com/runbook" }, { "type": "url", "uri": "https://example.com/status" } ] } ``` | `type` | `uri` means | |---|---| | `attachment` (or `file`) | absolute path to a file on the server | | `text` | literal text content | | `url` | URL string | ## Variable interpolation One syntax across all of InTouch: double-curly `{{ }}`. Reference a prior task's published output as `{{task-name.field}}` — the upstream task's `name:`, one dot, the field it published. ```json { "tasks": [ { "name": "fetch", "tool": "http", "properties": { "url": "https://api.example.com/data" } }, { "name": "notify", "tool": "message", "properties": { "userNames": [ "intouch" ], "subject": "Fetch result", "body": "Status {{fetch.statusCode}} at {{date(yyyy-MM-dd HH:mm)}}." } } ] } ``` Rules: - **No prefix.** `{{fetch.body}}` — never `{{task.fetch.body}}`, `{{tasks.fetch.body}}`, or `{{steps.fetch.body}}`. The literal prefixes `task`/`tasks`/`steps` are rejected and fail the workflow loud. - **Exactly one dot.** No nested paths (`{{a.b.c}}`), no bracket subscripts (`{{tasks['x'].y}}`). - `${ }` (dollar-brace) is **shell only** — InTouch never interpolates it. - Reserved namespaces (also `{{ }}`): `{{input.x}}`, `{{credential.x}}`, `{{workflow.name}}`, `{{args.x}}`, `{{env.NAME}}`, `{{date(pattern)}}`. - An unresolved reference **fails the workflow loud** — it is never delivered as literal text. Find a tool's published fields with `get_tool_readme()` — guessing leads to unresolved-reference failures. ## Where the file lives A Workflow File **is** a `bis_workflow` row whose IML lives on disk at `INTOUCH_HOME/workflows/files//workflow.iml` — the folder is named by the workflow's own **immutable id** (`bis_workflow.idx`), and supporting files (an auto-generated `README.md`, any scripts) live in that same `/` folder. **Disk is the source of truth for the IML.** The `bis_workflow.iml_content` DB column is only a legacy hydration fallback for pre-2026-04-23 rows — not where new Workflow Files are stored. (Legacy name-based files at `workflows/files/.yaml` and the older `workflows/iml/` path are read on startup and migrated automatically into id-based folders.) ## Automation is attached separately Scheduling and file-triggers are **not** declared in the IML — but the workflow exists the moment you install it, so attaching automation is straightforward: 1. **Install creates the workflow.** `POST /intouch/iml-workflow/install` always creates the `bis_workflow` row (to obtain its id) and writes `workflows/files//workflow.iml`. The workflow is runnable, schedulable, and trigger-fireable at once — there is no separate "activate" / "link-stub" step. It lands in the Default Area / Default Track. 2. **Attach a Schedule and/or Trigger File** to the workflow's `workflowId` via the Workflows view (or the schedule/trigger REST endpoints). Look up the `workflowId` with `get_workflow_by_name(name)`. Schedules and triggers link by `workflowId`. 3. **Convert (optional).** `POST /intouch/iml-workflow/convert` promotes the Workflow File in-place to a native workflow (`workflowTypeCode=0`) on the same `bis_workflow.idx`, materializing tasks/notifications/ content into real DB rows. The IML file is archived as provenance. Schedules/triggers attached before conversion survive, because they link by `workflowId`, which doesn't change. InTouch uses its own scheduler (types: day, week, weekday, weekend, month/specific days, month/relative days, custom) — **not cron**. See the schedule + trigger cookbook recipes for how to attach them. ## Install behavior `POST /intouch/iml-workflow/install` with the IML text as a `text/plain` body: 1. Parse IML → `ImlWorkflowDefinition`. 2. Validate (see [Validation rules](#validation-rules)). 3. Create the `bis_workflow` row (or reuse the existing one for a re-install of the same name) to obtain its id, write the file to `INTOUCH_HOME/workflows/files//workflow.iml`, and (re)generate its `README.md`. 4. Register/refresh the in-memory definition so the workflow is immediately runnable and schedulable. Notifications and content travel in the IML and fire at run time via `ImlWorkflowExecutor`; they are materialized into `bis_workflow_subscription` / `bis_workflow_content` rows at **convert** time, not at install. Re-installing the same `name` reuses the same workflow id and overwrites the file on disk. > Prefer the `install_iml_workflow` / `update_iml_workflow` assistant actions over calling the REST > endpoint directly, and never write to `INTOUCH_HOME/workflows/files/` yourself — direct disk > writes bypass validation. ## Validation rules The install endpoint rejects (HTTP 400, descriptive message) any of: - IML that doesn't parse. - `name` blank or containing path separators / shell metacharacters. - A task with `tool:` not registered in `TaskConnectorRegistry`. - A task whose tool requires a credential but `credential:` is blank. - Writing `type:` instead of `tool:` on a task. Warnings (install still succeeds): - Contact or credential name not found on the server (re-checked at execution time). - Empty `tasks:` (the workflow does nothing). Because unknown keys are silently dropped, a `schedule:`, `triggerFile:`, `after:`, or `onFailure:` block does **not** error — it is simply ignored. Don't write them; they have no effect. ## Hub publish conventions Beyond install requirements, InTouchHub publishing enforces: - `name` matches the hub slug. - `version` is a valid semver ≥ the last published version. - `description` non-empty (≥ 20 chars recommended). - `category` set (used for hub browse filtering). - `credential:` names use placeholders (e.g. `your-anthropic-credential`) so users rename them post-install. - No machine-specific absolute paths baked into `properties` — prefer `{{env.INTOUCH_HOME}}/...` or document the path in the README. ## Idiom patterns ### Fetch then notify (inbox) ```json { "name": "site-check", "version": "1.0.0", "description": "Hit a URL and post the status to the inbox.", "tasks": [ { "name": "probe", "tool": "http", "properties": { "method": "GET", "url": "https://blueisle.com" } }, { "name": "notify", "tool": "message", "properties": { "userNames": [ "intouch" ], "subject": "Site check", "body": "Status: {{probe.statusCode}} at {{date(yyyy-MM-dd HH:mm)}}" } } ] } ``` ### AI summarize then email ```json { "name": "rss-digest", "version": "1.0.0", "description": "Read a feed, summarize with AI, deliver.", "tasks": [ { "name": "read-feeds", "tool": "http", "properties": { "method": "GET", "url": "https://hnrss.org/frontpage" } }, { "name": "summarize", "tool": "anthropic", "credential": "anthropic", "properties": { "systemPrompt": "Summarize these headlines in 5 bullets.", "prompt": "{{read-feeds.body}}" } }, { "name": "deliver", "tool": "message", "properties": { "contactNames": [ "me" ], "subject": "Daily digest", "body": "{{summarize.completion}}" } } ] } ``` ### Run it on a schedule The schedule is **not** in the IML. Install the Workflow File (which creates the workflow), then: 1. `get_workflow_by_name(name)` to look up the workflow's `workflowId`. 2. `create_schedule(...)` + link it to that `workflowId` (via the Workflows view or the schedule API). ### Watch a value and AI-decide whether to alert → use a Monitor IML workflows have no "decide whether to fire" arm. For "alert me when X" install a **Monitor** (a separate entity) — see `cookbook/automate/monitor.md` and `cookbook/scenarios/alert-me-when.md`. --- See also: - `INTOUCH_REFERENCE.md` — IML Workflows overview and storage model - `IML_TOOL_REFERENCE.md` — authoring reusable tools (the building blocks of IML workflows) - `BUILDING_IML_TOOLS.md` — step-by-step authoring tutorial - `cookbook/build/workflow-file.md` — the workflow-authoring recipe