Skip to content

Advanced Form Configuration

User inputs can react to each other, to the entity the action runs on, and to the user running it — the same model as Port's advanced form configurations. Everything below lives in the workflow's trigger.userInputs and can be set either from the User Inputs builder (open an input → Advanced) or in Edit JSON mode.

Wherever a value is marked jqQuery-able you can write either a constant or { "jqQuery": "<expression>" }. Expressions are evaluated on the server (same jq engine as step templating) every time the form changes.

jq context

KeyContents
.formthe current values of the form (.form.environment). For format: "entity" inputs this is the selected entity{ identifier, title, blueprint, properties, relations } — so use .form.cluster.identifier, .form.cluster.properties.region or .form.cluster.relations.team
.entitythe target entity for DAY-2 / DELETE actions — identifier, title, blueprint, properties, relations; null for CREATE actions
.userwho is filling the form — userId, email, name, teams (identifiers of the catalog _team entities owning the user's _user entity)

Dependencies between inputs — dependsOn

json
{
  "cluster": {
    "type": "string",
    "format": "entity",
    "blueprint": "cluster",
    "dependsOn": ["environment"]
  }
}

cluster is disabled until environment has a value. When environment is cleared again the cluster value is cleared too. Set dependsOn on any input whose rules reference .form.<other input>.

Show / hide and disable — visible, disabled

json
{
  "reason": {
    "type": "string",
    "title": "Reason",
    "visible": { "jqQuery": ".form.environment == \"prod\"" }
  },
  "runbook": {
    "type": "string",
    "disabled": { "jqQuery": ".user.email | endswith(\"@platform.io\") | not" }
  }
}

Both accept a boolean ("visible": false always hides) or a jqQuery returning a boolean. Hidden inputs are never submitted and are never required; disabled inputs are read-only but their value (typically a default) is submitted.

Dynamic required, enum and default

json
{
  "required": {
    "jqQuery": "if .form.environment == \"prod\" then [\"environment\", \"reason\"] else [\"environment\"] end"
  },
  "properties": {
    "environment": { "type": "string", "enum": ["dev", "staging", "prod"] },
    "runtime": {
      "type": "string",
      "enum": {
        "jqQuery": "if .form.language == \"python\" then [\"3.11\", \"3.12\"] else [\"node18\", \"node20\"] end"
      },
      "dependsOn": ["language"]
    },
    "tags": {
      "type": "array",
      "default": { "jqQuery": ".entity.properties.tags" }
    }
  }
}
  • required (form level) — a jqQuery must return an array of input identifiers.
  • enum — a jqQuery must return an array; the dropdown re-renders as the form changes.
  • default — evaluated once when the form opens (handy for DAY-2 actions pre-filled from .entity).

enum can also depend on who is filling the form. .user.teams is the list of team identifiers the user belongs to, so a dropdown can offer different values per team — the platform re-checks the resolved list when the run starts, so a value that was not offered is rejected even if submitted through the API:

json
{
  "environment": {
    "type": "string",
    "title": "Environment",
    "enum": {
      "jqQuery": "if any(.user.teams[]?; . == \"admin\") then [\"dev\", \"staging\", \"prod\"] elif any(.user.teams[]?; . == \"developer\") then [\"dev\", \"staging\"] else [] end"
    },
    "default": {
      "jqQuery": "if any(.user.teams[]?; . == \"admin\") then [\"dev\", \"staging\", \"prod\"] elif any(.user.teams[]?; . == \"developer\") then [\"dev\", \"staging\"] else [] end | first"
    }
  }
}
  • any(.user.teams[]?; . == "admin") is true when the user is in the admin team ([]? keeps it safe when the list is empty). An empty array leaves the dropdown disabled for everyone else.
  • Piping the same expression into | first gives a matching default (null when nothing is offered, which leaves the field empty).
  • Team identifiers are the team names from Admin → Users and teams; see Deploy Selected Services for the demo that uses this.

Filtering entity inputs — dataset

json
{
  "cluster": {
    "type": "string",
    "format": "entity",
    "blueprint": "cluster",
    "dependsOn": ["environment"],
    "sort": { "property": "$title", "order": "ASC" },
    "dataset": {
      "combinator": "and",
      "rules": [
        { "property": "env", "operator": "=", "value": { "jqQuery": ".form.environment" } },
        { "property": "$team", "operator": "containsAny", "value": { "jqQuery": ".user.teams" } },
        { "property": "archived", "operator": "isEmpty" },
        { "relation": "region", "operator": "in", "value": ["eu-west", "eu-central"] },
        { "operator": "relatedTo", "blueprint": "service", "value": { "jqQuery": ".entity.identifier" } }
      ]
    }
  }
}

The entity picker is a searchable dropdown: the text you type is matched on title/identifier and combined with the dataset rules server-side (POST /blueprints/:bp/entities/search). While a rule references an input that is still empty the picker shows "Fill in … first".

Chaining entity inputs

Because .form.<entityInput> is the selected entity, one picker can be narrowed by another one:

json
{
  "cluster": { "type": "string", "format": "entity", "blueprint": "cluster" },
  "service": {
    "type": "string", "format": "entity", "blueprint": "service",
    "dependsOn": ["cluster"],
    "dataset": {
      "combinator": "and",
      "rules": [
        { "relation": "cluster", "operator": "=", "value": { "jqQuery": ".form.cluster.identifier" } },
        { "property": "region", "operator": "=", "value": { "jqQuery": ".form.cluster.properties.region" } },
        { "property": "$team", "operator": "containsAny", "value": { "jqQuery": "[.form.cluster.relations.team]" } }
      ]
    }
  }
}

relations hold the target entity identifiers (a string, or an array when a relation has several targets). If a rule value resolves to a whole entity (e.g. .form.cluster), its identifier is used.

Rule shape

KeyMeaning
propertya blueprint property, or a meta property: $title, $identifier, $blueprint, $team (owning team identifiers)
relationinstead of property — filters on the identifier of the related entity
operatorsee below
valueconstant or { "jqQuery": … }; omitted for isEmpty / isNotEmpty
blueprintwith relatedTo: restrict to related entities of that blueprint

Operators

OperatorApplies to
=, !=any scalar property, meta properties, relations
>, >=, <, <=numbers / dates
contains, doesNotContainsstrings (case-insensitive substring), arrays (membership)
containsAnyarray properties, $team
beginsWith, endsWithstrings
in, notInscalar properties and relations (value is an array)
between, notBetweennumbers / dates (value is [min, max])
isEmpty, isNotEmptyany property / relation (no value)
relatedToentities linked (in either direction) to the entity identifier in value, optionally limited to blueprint

Sorting entity inputs — sort

sort controls the order of the entities in the dropdown, exactly like Port:

json
"sort": { "property": "tier", "order": "DESC" }
  • property is either a meta property ($title, $identifier, $createdAt, $updatedAt) or the identifier of a property of the target blueprint. object and array properties cannot be sorted on.
  • order is ASC (default) or DESC.
  • When sort is omitted the entities are sorted by title, ascending.
  • Entities with no value for the property are always listed last.

In the editor, the Sort entities field under the blueprint selector exposes the same options (meta properties + the blueprint's scalar properties, and the direction).

TIP

Meta properties are ordered by the database. Blueprint properties are stored as JSON and are sorted in memory: the search loads the entities matching the dataset (capped at 5 000) before ordering them, so prefer a dataset filter or a meta property on very large blueprints.

Selecting several entities — type: "array"

An input can accept any number of entities of one blueprint. Port-style, the entity configuration moves onto items and the property itself becomes an array:

json
{
  "repos": {
    "type": "array",
    "title": "Repositories",
    "items": {
      "type": "string",
      "format": "entity",
      "blueprint": "githubRepository",
      "sort": { "property": "$updatedAt", "order": "DESC" },
      "dataset": {
        "combinator": "and",
        "rules": [{ "property": "$identifier", "operator": "in", "value": ["idpnextdemo/orders-service", "idpnextdemo/payments-service"] }]
      }
    }
  }
}
  • items.blueprint is required. dataset and sort work exactly as above but must sit on items — putting them on the property is rejected when the workflow is saved.
  • In the editor, choose Limit → Multiple entities under the blueprint selector; the builder writes the type: "array" + items shape for you. Array inputs have no Default field.
  • The portal renders a multi-select picker: a searchable checkbox list that stays open while you pick, with the selection shown as chips on the field (+N more after three).
  • A required array input must contain at least one entity — an empty selection counts as missing.

The Bulk Repo Audit form with its multi-entity picker

The value is available in two shapes, so pick the right one:

Expression.repos is…
.inputs.repos (step config templates)an array of entity identifiers (["idpnextdemo/orders-service", …]), in the order they were picked
.form.repos (form rules — jqQuery in visible, required, dataset, … — and step config templates)an array of hydrated entities ([{ identifier, title, properties, relations, … }, …]); an identifier that no longer exists is left as a plain string

The same applies to a single-entity input: {{ .inputs.release }} is the identifier, {{ .form.release.properties.tag }} reads the selected entity. In step templates .form is a snapshot taken when the run starts.

Ocean identifiers carry the owner

Entities synced by Ocean use org/name identifiers. When an external system wants bare names, map them in the template: {{ .inputs.repos | map(split("/") | last) }}["orders-service", …]. The same trick works on a single item inside a loop: {{ .item | split("/") | last }}.

See the Bulk Repo Audit and Deploy Selected Services demo workflows for complete definitions.

Full example

json
{
  "order": ["environment", "cluster", "reason"],
  "required": {
    "jqQuery": "if .form.environment == \"prod\" then [\"environment\", \"cluster\", \"reason\"] else [\"environment\", \"cluster\"] end"
  },
  "properties": {
    "environment": { "type": "string", "title": "Environment", "enum": ["dev", "staging", "prod"] },
    "cluster": {
      "type": "string", "title": "Cluster", "format": "entity", "blueprint": "cluster",
      "dependsOn": ["environment"],
      "dataset": {
        "combinator": "and",
        "rules": [
          { "property": "env", "operator": "=", "value": { "jqQuery": ".form.environment" } },
          { "property": "$team", "operator": "containsAny", "value": { "jqQuery": ".user.teams" } }
        ]
      }
    },
    "reason": {
      "type": "string", "title": "Reason for a production deploy",
      "visible": { "jqQuery": ".form.environment == \"prod\"" }
    }
  }
}

Errors and edge cases

  • A jq expression that fails (syntax or runtime) never blocks the form: the input falls back to visible: true / disabled: false, a static enum/required if any, and the form shows an amber "Some form rules could not be evaluated" banner. The failing expression is logged server-side.
  • dependsOn referencing an unknown input, an input depending on itself, or a cycle is rejected when the workflow is saved.
  • dataset is only allowed on format: "entity" inputs; unknown operators are rejected on save.
  • For type: "array" inputs, items.blueprint is required when items.format is "entity", and sort/dataset must be on items"for an entity array, sort/dataset belong on "items", not the property itself".
  • The server re-evaluates the form when the run starts: values of hidden or dependency-blocked inputs are dropped and the dynamic required list is enforced, whatever the client sent.

IDP Next — Internal Developer Platform