Flow Definition Schema
This reference describes the JSON shape accepted by O2ID flow definitions. For a usage-focused introduction, see Flows.
Flow resource
A flow resource wraps a definition with tenant metadata:
{
"name": "Login",
"trigger": "login",
"enabled": true,
"allowedClientIds": [],
"definition": {
"start": "identify",
"nodes": {}
}
}
| Field | Required | Description |
|---|---|---|
name | Yes | Human-readable flow name, unique within the tenant |
trigger | Yes | Flow trigger name. Use reserved system trigger values such as login for O2ID-invoked journeys; use names such as step-up or age-verification for custom journeys. |
definition | Yes | Flow graph definition |
allowedClientIds | No | Client IDs allowed to start this flow. Empty or omitted means no client allowlist |
enabled | No | Defaults to true |
Definition object
The definition object has two required fields, plus one optional field
that controls whether the flow may be started with no authenticated caller:
{
"start": "identify",
"nodes": {
"identify": {
"type": "authenticator",
"id": "identifier",
"onSuccess": "password",
"onFailure": "identify"
}
}
}
| Field | Required | Description |
|---|---|---|
start | Yes | ID of the first node to execute |
nodes | Yes | Object whose keys are node IDs and whose values are node definitions |
allowUnauthenticated | No | Whether this flow may be started with no known subject. Omit to use the default: true only for the login trigger, false for every other trigger. Set explicitly to opt any trigger in or out |
allowUnauthenticated is what lets /oauth2/authorize's ?prompt= param
(see Flows) launch a trigger other than login for an
unauthenticated user — the trigger name itself carries no special meaning;
only this flag, and whether the caller already has a subject, do.
Node IDs are local to the flow definition. They are referenced by fields such
as onSuccess, onFailure, properties.onLockout, condition branch then,
and condition else.
Node shape
Every node has the same top-level shape: type, an optional id naming
the resource this node configures (an authenticator ID for
authenticator, a connection ID for connection — left unset for node
types with no such resource), the control-flow fields that type uses
(onSuccess/onFailure for linear nodes, branches/else for
condition), properties for anything else node-type-specific, and an
optional outputs map — every node type accepts outputs, regardless of
what's in properties. Keeping configuration under properties (or, for
the one resource reference a node type has, under the shared id field)
means new node types add fields there instead of growing the shared node
shape. See Node outputs for what outputs does.
{
"type": "authenticator",
"id": "<totp-authenticator-id>",
"onSuccess": "allow",
"onFailure": "totp",
"properties": {
"maxAttempts": 5,
"onLockout": "deny"
}
}
id also doubles as the selection key when this node appears inline
inside a choice node's options array (see choice) — there
is no separate "option key" concept, an option is just a node.
Node references
Most transitions are plain string references:
{
"onSuccess": "allow",
"onFailure": "password"
}
Condition branch then and condition else also support inline condition
objects:
{
"type": "condition",
"branches": [
{
"if": "'totp' in user.enrolledFactors",
"then": "totp"
}
],
"else": {
"type": "condition",
"branches": [
{
"if": "tenant.mfaRequired",
"then": "totp"
}
],
"else": "allow"
}
}
authenticator.onSuccess, authenticator.onFailure, and
authenticator.properties.onLockout are string node IDs.
Supported node types
| Type | Purpose |
|---|---|
authenticator | Resolves the user by their login identifier, or verifies built-in password or a configured TOTP authenticator |
condition | Evaluates CEL expressions and chooses the next node |
outcome | Completes the flow run |
flow | Jumps into another flow, resuming this one when it completes |
script | Runs tenant-authored Starlark logic in-process, no network/filesystem access |
webhook | Calls a tenant-supplied HTTP endpoint; its response becomes the step result |
connection_call | Calls an operation on a registered connection (e.g. a remote connection type); its response becomes the step result |
credential | Grants a credential (currently: an OID4VCI credential offer) to the run's subject |
choice | Presents several sign-in options at once; the client picks which one runs |
organization_context | Detects a nested Organization tenant and shows a "signing in to" banner naming it |
Use metadata to discover the node types accepted by the running O2ID instance.
authenticator
An authenticator node resolves the user's identifier, or verifies a
credential or factor.
Built-in identifier step — resolves a user without verifying a credential.
Useful when a later node needs to act on the resolved user (branch on
user.enrolledFactors, tenant policy, etc.) before deciding what to verify
next:
{
"type": "authenticator",
"id": "identifier",
"onSuccess": "password",
"onFailure": "identify"
}
Built-in password step:
{
"type": "authenticator",
"id": "password",
"onSuccess": "allow",
"onFailure": "password",
"properties": {
"maxAttempts": 5,
"onLockout": "deny"
}
}
TOTP step, using the ready-to-use built-in instance (see below — no setup required):
{
"type": "authenticator",
"id": "totp",
"onSuccess": "allow",
"onFailure": "totp",
"properties": {
"maxAttempts": 5,
"onLockout": "deny"
}
}
| Field | Required | Description |
|---|---|---|
type | Yes | Must be authenticator |
id | Yes | identifier to resolve the user without a credential, password for the built-in password step, or a configured authenticator's ID |
onSuccess | Yes | Node ID to execute after successful verification |
onFailure | No | Node ID to execute after failed verification. Often points back to the same node |
properties.maxAttempts | No | Number of failed attempts before properties.onLockout is used |
properties.onLockout | No | Node ID to execute after maxAttempts failures |
id is one of:
identifier— the built-in identifier steppassword— the built-in password steptotp— a ready-to-use, zero-config TOTP factor every tenant gets for free (default issuer "O2ID", 6 digits, 30-second period). It's virtual — never created viaPOST /authenticators, and can't be updated or deleted — so create a separatetotp-type instance instead if you need different settings (see Managing Authenticators)- the
idof a tenant-configuredAuthenticatorinstance — never an authenticator type name directly
To discover valid values:
- Configured instances (and their IDs):
GET /authenticators(o2idctl authenticators list) — always includes the built-intotpfirst, even before anything is explicitly created - Types available to configure:
GET /authenticators/meta(o2idctl authenticators list types) — todaytotp(a synchronous code-entry factor, see the "Configured TOTP step" example above) andverifiable_credential_presentation(an out-of-band factor — see Out-of-band verification below)
The built-in password step doubles as an identify step when it's reached
with no user already resolved (i.e. no identifier authenticator ran
before it in this run) — it accepts identifier (or username) alongside
password in the same submission and resolves the user itself, instead of
requiring a separate identifier step first. This is what O2ID's implicit
default login flow uses, so a plain login renders as one combined form
rather than two screens. nodeView.fields reflects which shape is
expected: ["identifier", "password"] when combined, or just ["password"]
when an earlier identifier authenticator already resolved the user. Use a
separate identifier authenticator instead when you need to act on the
resolved user (e.g. branch on user.enrolledFactors) before verifying the
password.
Expected identifier submission — POST /flow-runs/{id}/submit:
{ "values": { "identifier": "alice.chen" } }
Submit the value of whatever field the user's type designates as its login
identifier — username for the built-in user type, or whatever a custom
type's usernameField names.
Expected password submission:
{ "values": { "password": "secret" } }
Or, standing alone with no prior identifier authenticator:
{ "values": { "identifier": "alice.chen", "password": "secret" } }
Expected TOTP submission:
{ "values": { "code": "123456" } }
If the referenced TOTP authenticator has progressiveEnrollment enabled
(see TOTP authenticator) and the subject has no
verified enrollment yet, the node enrolls them in place instead of failing
the code. First submit an empty values object to get setup material:
{ "values": {} }
nodeView.data then contains otpauthUrl and secret, and
nodeView.fields becomes ["enrollmentId", "code"]. After the user scans
the setup URI, submit both:
{ "values": { "enrollmentId": "<enrollment-id>", "code": "123456" } }
Out-of-band verification (e.g. credential presentation)
Some authenticator types verify asynchronously instead of via a
client-submitted code — today, one such type: verifiable_credential_presentation
(see Verifying Credentials), which asks the
subject to present a Verifiable Credential from their wallet (OID4VP).
Referencing one of these from an authenticator node works exactly like
referencing TOTP — same id/maxAttempts/onLockout
fields — but the node pauses instead of rendering fields: nodeView.data.requestUri
carries an openid4vp:// deep link to render as a QR code, and
nodeView.fields is absent (there's nothing to submit). The client
should never call POST /flow-runs/{id}/submit on this node — only the
authenticator's own out-of-band callback (POST /oid4vp/response)
resumes it, so the portal polls GET /flow-runs/{id} instead until the
view changes.
Context written:
| Path | Description |
|---|---|
step.<nodeId>.result | success or failure |
step.<nodeId>.attempts | Failed attempt count |
step.<nodeId>.claims | An out-of-band authenticator's disclosed claims on success (e.g. a presented credential's claims) |
user.id | Resolved user ID — set by the identifier and password authenticators |
user.userType | The user's assigned user type key, e.g. employee — always set once resolved |
user.username | The value of whatever field the user's type designates as its login identifier |
user.enrolledFactors | Verified factor types for the user, such as totp |
user.attributes | User attributes available to the run — email, when present, is always at user.attributes.email |
condition
A condition node evaluates Google Common Expression Language (CEL)
expressions in order. The first branch whose if expression evaluates to
true wins. If no branch matches, else is used.
{
"type": "condition",
"branches": [
{
"if": "'totp' in user.enrolledFactors",
"then": "totp"
},
{
"if": "tenant.mfaRequired",
"then": "totp"
}
],
"else": "allow"
}
| Field | Required | Description |
|---|---|---|
type | Yes | Must be condition |
branches | Yes | Ordered list of branches |
branches[].if | Yes | CEL expression |
branches[].then | Yes | Node reference, or inline condition object |
else | Yes | Node reference, or inline condition object |
Clients do not receive a nodeView for a condition node. O2ID evaluates the
condition and advances the run to the selected next node.
Condition context
Conditions can read these context variables:
| Path | Type | Description |
|---|---|---|
user.id | string | Resolved user ID |
user.userType | string | The user's assigned user type key, e.g. employee — always set |
user.username | string | The value of whatever field the user's type designates via usernameField |
user.enrolledFactors | list<string> | Verified factor types for the user, such as totp |
user.attributes | map | User attributes available to the run — email, when present, is always at user.attributes.email |
tenant.mfaRequired | bool | Tenant MFA policy flag. Defaults to false when no policy value is available |
step.<nodeId>.result | string | success or failure for a completed step |
step.<nodeId>.attempts | int | Failed attempt count recorded by that step |
step.<nodeId>.outputs.<name> | any | A named value declared by that node's outputs — see Node outputs |
risk.score | int | Risk score. Defaults to 0 when no risk signal is available |
Example expressions:
'totp' in user.enrolledFactors
step.password.result == 'success' && step.password.attempts == 0
tenant.mfaRequired || risk.score > 50
outcome
An outcome node completes the run.
Allow or deny:
{
"type": "outcome",
"properties": {
"result": "allow"
}
}
Claims-producing result:
{
"type": "outcome",
"properties": {
"result": "success",
"claims": {
"ageOver18": true
}
}
}
| Field | Required | Description |
|---|---|---|
type | Yes | Must be outcome |
properties.result | Yes | Result string, such as allow, deny, or success |
properties.claims | No | JSON object returned with the outcome |
deny marks the flow run as failed. Other result values complete the run.
flow
A flow node jumps into another flow, identified by its trigger, and
resumes this flow's onSuccess/onFailure when that flow's run reaches an
outcome node — the same way onSuccess/onFailure resume after any other
node type, just with an entire other flow run in between.
The target flow is resolved exactly like POST /flow-runs resolves its own
trigger: the active, enabled flow for that trigger, falling back to the
built-in login flow for login when no custom one is active, or a
not-found error otherwise. It starts at that flow's own start node unless
properties.nodeId names a different node in it.
{
"type": "flow",
"onSuccess": "allow",
"onFailure": "deny",
"properties": {
"trigger": "step-up",
"nodeId": "totp"
}
}
| Field | Required | Description |
|---|---|---|
type | Yes | Must be flow |
onSuccess | Yes | Node ID to execute when the target flow's run completes with a non-deny result |
onFailure | No | Node ID to execute when it completes with deny. If omitted, a deny ends this run entirely, exactly as if this had been a top-level outcome node |
properties.trigger | Yes | Trigger of the flow to jump into |
properties.nodeId | No | Node ID within the target flow's definition to start at, instead of its start |
Clients do not receive a nodeView for a flow node itself — the run keeps
rendering whatever node the target flow lands on next, same as condition.
Context written:
| Path | Description |
|---|---|
step.<nodeId>.result | success or failure, from the target flow's outcome result |
step.<nodeId>.claims | The target flow's outcome claims, if it set any |
Nesting is capped at 10 levels deep — a flow node whose trigger (directly
or transitively) leads back to itself fails with a recursion-limit error
once the cap is hit, since two flows referencing each other's triggers
can't be caught at authoring time (each would need the other to already be
active to validate).
script
A script node runs tenant-authored Starlark
source in-process. Starlark has no network or filesystem access by default,
so a script node can't reach anything outside the flow run's own context —
unlike condition's CEL expressions, Starlark is Turing-complete, so
properties.maxSteps and properties.timeoutMs bound how much it can run.
{
"type": "script",
"onSuccess": "allow",
"onFailure": "deny",
"properties": {
"source": "result = {'riskAdjusted': risk['score'] > 50}",
"maxSteps": 100000,
"timeoutMs": 2000
}
}
| Field | Required | Description |
|---|---|---|
type | Yes | Must be script |
onSuccess | Yes | Node ID to execute when the script sets a result |
onFailure | Yes | Node ID to execute when the script raises an exception, fails to set result, or exceeds its step/time limit |
properties.source | Yes | Starlark source. Must assign a top-level result global — that value becomes step.<nodeId>.result |
properties.maxSteps | No | Abstract computation step ceiling. Defaults to a server-side value; always clamped to a hard maximum regardless of what's configured |
properties.timeoutMs | No | Wall-clock ceiling in milliseconds. Same defaulting/clamping as maxSteps |
Unlike Starlark's default BUILD-file dialect, if/for/while are allowed
at the top level and reassigning a top-level name (total = 0 then later
total = total + 1) is allowed too — a script node's source is a short
imperative program, not a config file being loaded as a module, so it reads
like an ordinary function body without needing to be wrapped in a def.
The script reads the same user/tenant/step/risk values condition
nodes evaluate against (see Condition context) as
predeclared globals, and nothing else — no per-node-type secret smuggling.
Clients do not receive a nodeView for a script node — like condition,
it runs synchronously and the run keeps rendering whatever node comes next.
Context written:
| Path | Description |
|---|---|
step.<nodeId>.result | The script's result value, whatever shape it is |
step.<nodeId>.error | Failure reason, set instead of result when the node routed to onFailure |
webhook
A webhook node calls a tenant-supplied HTTP endpoint and uses its response
as the step result.
{
"type": "webhook",
"onSuccess": "allow",
"onFailure": "deny",
"properties": {
"url": "https://verify.example.com/check",
"method": "POST",
"timeoutMs": 5000,
"headers": { "X-Api-Key": "..." }
}
}
| Field | Required | Description |
|---|---|---|
type | Yes | Must be webhook |
onSuccess | Yes | Node ID to execute after a 2xx response with a valid JSON body |
onFailure | Yes | Node ID to execute on any other outcome — network error, non-2xx status, an oversized or malformed body |
properties.url | Yes | Absolute http:// or https:// URL |
properties.method | No | Defaults to POST |
properties.timeoutMs | No | Request timeout in milliseconds. Defaults to a server-side value; always clamped to a hard maximum |
properties.headers | No | Extra request headers |
The request body is the same user/tenant/step/risk context CEL
conditions and script nodes read, JSON-encoded. Every request carries an
X-O2ID-Signature header — an HMAC-SHA256 of the request body under a key
derived from the tenant, so the receiver can verify the call actually came
from this tenant's flow run. The response body is size-capped (64KB) and
must be JSON.
SSRF guard: by default, a webhook node cannot reach a
private/loopback/link-local/unspecified/multicast address, no matter what
properties.url says — this blocks a flow author from using a webhook to
probe or call internal infrastructure. To call an internal endpoint
deliberately, add its hostname to the tenant's webhook allowlist:
o2idctl tenant-settings update --webhook-allowlist internal-service.example.internal
--webhook-allowlist is repeatable — pass it once per host to allow more
than one. --clear-webhook-allowlist removes every host. Equivalently,
PATCH /settings accepts the same field directly:
PATCH /settings
{ "webhookAllowlist": ["internal-service.example.internal"] }
An allowlisted hostname is exempted from the address check entirely — only add hosts the tenant genuinely intends flows to reach.
Clients do not receive a nodeView for a webhook node — like condition,
it runs synchronously and the run keeps rendering whatever node comes next.
Context written:
| Path | Description |
|---|---|
step.<nodeId>.result | The parsed JSON response body |
step.<nodeId>.error | Failure reason, set instead of result when the node routed to onFailure |
connection_call
A connection_call node calls an operation on a connection and uses its
response as the step result — the generic counterpart to
connection's redirect-based
federated login. Unlike connection, it works with any connection whose
type declares the operation it names, not only an identity-provider one:
send an SMS, verify a document, look up a value in an external system, and
so on. See Connection Types for
how a connection gets an operation to call in the first place.
{
"type": "connection_call",
"id": "acme-idv-connection",
"onSuccess": "allow",
"onFailure": "deny",
"properties": {
"operation": "verify",
"input": { "documentId": "step.upload.outputs.documentId" },
"timeoutMs": 8000
}
}
| Field | Required | Description |
|---|---|---|
type | Yes | Must be connection_call |
id | Yes | ID of a connection whose type declares properties.operation among its operations |
onSuccess | Yes | Node ID to execute after a successful call |
onFailure | Yes | Node ID to execute on any other outcome — network error, an unresolvable connection, an operation the connection's type doesn't declare, or the connector's own reported failure |
properties.operation | Yes | Operation name, as declared by the connection's type (GET /connections/meta, or o2idctl connections types) |
properties.input | No | Map of input key to a CEL expression, evaluated in the same user/tenant/step/risk environment as a condition node's branches |
properties.timeoutMs | No | Request timeout in milliseconds. Defaults to a server-side value; always clamped to a hard maximum |
Clients do not receive a nodeView for a connection_call node — like
webhook, it runs synchronously and the run keeps rendering whatever node
comes next.
Context written:
| Path | Description |
|---|---|
step.<nodeId>.result | The operation's JSON-object output |
step.<nodeId>.error | Failure reason, set instead of result when the node routed to onFailure |
credential
A credential node grants a credential to the run's current subject as a
side effect of the flow — e.g. an age-verification or step-up sequence —
instead of requiring a separate API call after the fact. properties.kind
selects which kind of credential is granted and which other properties
apply.
verifiable_credential is the only kind implemented today: it creates an
OID4VCI credential offer against a
verifiable credential you've already
defined, instead of requiring a separate POST /verifiable-credential-offers call.
{
"type": "credential",
"onSuccess": "allow",
"onFailure": "deny",
"properties": {
"kind": "verifiable_credential",
"verifiableCredentialId": "verifiable-credential-id",
"claimsFromContext": ["ageOver18"],
"requireTxCode": false
}
}
| Field | Required | Description |
|---|---|---|
type | Yes | Must be credential |
onSuccess | Yes | Node ID to execute once the client acknowledges the credential offer |
onFailure | Yes | Node ID to execute on any failure — unknown credential kind, unknown or disabled verifiable credential, or no credential issuance configured on this server |
properties.kind | Yes | Which kind of credential to grant. Only verifiable_credential is implemented today |
properties.verifiableCredentialId | Yes, for kind verifiable_credential | The verifiable credential to issue against — must exist and be enabled |
properties.claimsFromContext | No, kind verifiable_credential only | Context keys to lift into the offer's claims (e.g. a value an earlier script/webhook/outcome step wrote to context). A name with no matching key in context is silently omitted, not an error — the same graph can be reused across scenarios where not every claim is always populated |
properties.requireTxCode | No, kind verifiable_credential only | Requests a short numeric PIN alongside the QR code — see Requiring a tx_code PIN |
Every claim name in claimsFromContext that does resolve to a value must
still be one of the verifiable credential's own defined claim names —
the exact same subset-of-schema validation POST /verifiable-credential-offers applies.
Unlike webhook/script, a credential node pauses on success: the
run parks on this node and the client receives a nodeView of type
credential whose data is the OID4VCI credential_offer object itself
(credential_issuer, credential_configuration_ids, grants,
credential_offer_uri), plus data.deepLinkUri (the
openid-credential-offer:// URI to render as a QR code) and
data.txCode (the raw PIN, only present when requireTxCode was set)
for the portal to display. The client then submits an empty Advance to
continue to onSuccess. A failure never pauses — it routes straight to
onFailure, the same as webhook.
Context written:
| Path | Description |
|---|---|
step.<nodeId>.result | The OID4VCI credential_offer object — the same shape POST /verifiable-credential-offers returns |
step.<nodeId>.error | Failure reason, set instead of result when the node routed to onFailure |
choice
A choice node presents several sign-in options at once and lets the
client pick which one runs, instead of O2ID deciding via a condition
node's server-evaluated CEL branches. Each entry in properties.options
is a full node — its own id/type/properties/onSuccess/onFailure
— not a separate wrapper shape, since an option is simply a node with no
name of its own in the top-level nodes map.
{
"type": "choice",
"properties": {
"options": [
{ "id": "password", "type": "authenticator", "onSuccess": "allow" },
{ "id": "<connection-id>", "type": "connection", "onSuccess": "allow", "onFailure": "deny" },
{ "id": "org_sso", "type": "organization_context", "onSuccess": "allow" }
]
}
}
| Field | Required | Description |
|---|---|---|
type | Yes | Must be choice |
properties.options | Yes | At least one inline node, each with a unique id. Supported option types: authenticator, connection, organization_context |
A choice node itself has no onSuccess/onFailure — control flow
comes entirely from whichever option's own onSuccess/onFailure runs.
An option's onSuccess/onFailure are node IDs in the outer
nodes map, exactly like a top-level node's — an option is not a nested
sub-graph.
The nodeView for a choice node carries only id/type per option —
no display label:
{
"type": "choice",
"data": {
"options": [
{ "id": "password", "type": "authenticator" },
{ "id": "org_sso", "type": "organization_context" }
]
}
}
Deciding what each option looks like (button text, icon) is the client's
responsibility, keyed off id/type — the flow definition never
authors a label. Submit which option was picked to advance:
{ "values": { "choice": "password" } }
organization_context
An organization_context node detects whether the currently-running flow
was reached via a nested Organization's own path (see
Organizations) and,
if so, surfaces a "signing in to" banner naming it before
continuing. Normally used as one option inside a choice node rather
than as a standalone entry node. On a run not reached via an
Organization path, it's a pure pass-through: it succeeds immediately with
no pause at all, as if the node weren't there.
{
"type": "organization_context",
"onSuccess": "allow",
"properties": {
"mode": "banner"
}
}
| Field | Required | Description |
|---|---|---|
type | Yes | Must be organization_context |
onSuccess | Yes | Node ID to execute once the banner is acknowledged (or immediately, on a pass-through run) |
properties.mode | No | banner (default) shows the "signing in to" step naming the Organization. redirect/auto are reserved for a future release — validation rejects them today |
When it pauses, nodeView.data carries the Organization's display info:
{
"type": "organization_context",
"data": {
"organizationName": "Acme, Inc.",
"organizationSlug": "acme"
}
}
The client submits an empty Advance to continue, the same idiom
credential nodes use for their own zero-input continue step.
Node outputs
Any node can declare outputs: a map of name to CEL expression, evaluated
in the same context as a condition's if (see Condition
context above), plus this node's own step.<nodeId>
entry, once that node completes. A later node reads the computed value at
step.<nodeId>.outputs.<name>.
{
"type": "webhook",
"onSuccess": "check_risk",
"onFailure": "deny",
"properties": {
"url": "https://risk.example.com/score"
},
"outputs": {
"highRisk": "step.risk_check.result.score > 70"
}
}
step.risk_check.outputs.highRisk
Outputs are computed only on a node's success path — a failed node's
step.<nodeId> entry has error instead of result/claims, so an
expression written against the success shape has nothing to read. An empty
or invalid expression fails flow validation at /nodes/<id>/outputs/<name>.
Validation
o2idctl flows validate <flow-id> checks:
| Check | Description |
|---|---|
| Definition shape | start and nodes are present and valid JSON |
| Node references | Every referenced node ID exists |
| Authenticator IDs | Referenced configured authenticators exist |
| Flow references | A flow node's trigger resolves to an active flow, and its nodeId (if set) exists in that flow |
| CEL expressions | Condition expressions compile |
| Script syntax | script node source parses as valid Starlark (parsed, not executed) |
| Webhook URLs | webhook node properties.url is an absolute http(s) URL |
| Connection call operations | connection_call node properties.operation is declared by the referenced connection's type |
| Credential kind | credential node properties.kind is a recognized kind |
| Verifiable credential IDs | A verifiable_credential-kind credential node's properties.verifiableCredentialId is present |
| Reachability | Every node is reachable from start |
| Unsupported nodes | Node types not listed by metadata are rejected |
Validation errors include a JSON Pointer so tooling can identify the exact field that failed:
{
"valid": false,
"errors": [
{
"pointer": "/nodes/mfa_check/branches/0/if",
"message": "ERROR: ..."
}
]
}
Metadata
Use metadata to discover the node types and context variables supported by the running O2ID instance:
o2idctl flows meta
The API also exposes a downloadable schema document:
GET /flows/meta/schema.json