GraphQL API
This page lists everything the IdentityFlow 0.3.1 GraphQL API offers a UI of your own: the endpoint, how requests are authenticated, the eight read views, the seven mutations, what each requires and which errors it returns. For a worked example, start with Build your own UI.
Your UI must share IdentityFlow’s origin and browser session. There are no API keys or bearer tokens in 0.3.1. Start with read views, mutations or error behavior; the endpoint and session requirements follow below.
The IdentityFlow program serves the API at /graphql on its own origin. Send a
POST with content-type: application/json and a body
{ "query": "…", "variables": { … } }.
A request is authenticated only by the identity_flow_session cookie that the
OIDC sign-in at /auth/login sets. The cookie is HttpOnly and
SameSite=Lax, its path is /, and it is Secure when
auth.oidc.redirectUri uses HTTPS. A session lasts auth.oidc.sessionTtl, at
most 8 hours; see Configuration. 0.3.1 has no
bearer token, API key or other credential; a request needs the session cookie
of a signed-in browser. Without a valid session every root field returns
UNAUTHENTICATED.
0.3.1 is built for a UI served from the same origin as IdentityFlow; see
Build your own UI. Browsers do not
send the SameSite=Lax cookie with requests from other sites, but they do send
it with same-site requests, including from another subdomain or port. At
/graphql, OPTIONS requests return 405, and responses to other origins
include no Access-Control-Allow-* headers. SvelteKit rejects cross-origin
form, multipart and text/plain POSTs with 403. CORS controls which browser
pages can read responses; it is not server-side authorization.
The program rejects introspection queries, and 0.3.1 ships no schema file. The
three schema blocks on this page, views,
mutations and types, are the complete schema.
Put them into one .graphql file if your client tooling needs a schema.
The program limits each query to a depth of 20, 1000 tokens, 15 aliases, 50 directives and a calculated cost of 5,000. A query over a limit is rejected before it runs; see Errors.
Each root field of Query is a view: what the signed-in account may see from
one angle. list returns a page of summaries and find(id) one record with
details.
| Root | Returns | Access | find returns |
|---|---|---|---|
startCatalog | Workflow Definitions the account may start | starter | StartableWorkflow, with description and inputContract |
initiationHistory | Instances the account started | the account started them | InitiationDetail, with input and result |
assignedDialogs | Pending dialogs the account may answer | one of the account’s principals is an assignee | DialogDetail, with taskContext |
participationHistory | Dialogs the account answered, taken from the steps it finished | the account answered them | ParticipationDetail, with taskContext and decision |
business | Instances with their business data | viewer | BusinessInstanceDetail, with businessData and steps |
operations | Instances with their operational state | operator | OperationsDetail, with message, until, diagnostics |
audit | Instances with their recorded evidence | auditor | AuditDetail, with eventCount and evidence |
deadLetters | Failed assignee source jobs of one instance, via list(instanceId) | operator | no find |
starter, viewer, operator and auditor are the
access functions granted by your
access configuration, per Workflow Definition name and release channel. deadLetters.list returns a plain
list without paging; its revision is the expectedRevision for
replayDeadLetter. businessData is null for an instance that has no
business data, such as an active instance that was never paused, and for an
instance that failed with an Error, such as most ERRORED instances. A
failure recorded as plain data, such as a thrown object, is returned as is.
first and after work the same in every paged view, for example
assignedDialogs:
list(first: Int = 100, after: String, sort: CreatedAtSortInput)- 1 to 100, default 100
- previous endCursor
| Argument or field | Notes |
|---|---|
first | 1 to 100, default 100. Another value returns BAD_USER_INPUT first must be between 1 and 100. |
after | The pageInfo.endCursor of the previous page. Leave it out or pass null for the first page. A malformed cursor returns BAD_USER_INPUT Invalid pagination cursor. |
sort | Instance and dialog views sort by creation time, newest first unless you pass direction: ASC. startCatalog sorts by NAME ascending unless you pass another field or direction. |
filter | names and releaseChannels on startCatalog; statuses and workflowNames on the instance views. assignedDialogs and participationHistory have no filter. |
items | The records of this page, in the order you asked for. |
pageInfo | hasNextPage says whether another page follows; endCursor is the cursor to pass as after for it. |
To read a whole list, request pageInfo { hasNextPage endCursor } with the
items and repeat the query with after: endCursor while hasNextPage is
true. Keep sort and filter the same on every page: the cursor marks a
position in that order. Lists have no count of all records and no facets;
selecting total or facets returns GRAPHQL_VALIDATION_FAILED.
type StartCatalogView { list( first: Int = 100 after: String sort: WorkflowSortInput filter: WorkflowFilterInput ): StartCatalogPage! find(id: ID!): StartableWorkflow!}
type InitiationHistoryView { list( first: Int = 100 after: String sort: CreatedAtSortInput filter: InstanceFilterInput ): InitiationPage! find(id: ID!): InitiationDetail!}
type AssignedDialogsView { list(first: Int = 100, after: String, sort: CreatedAtSortInput): DialogPage! find(id: ID!): DialogDetail!}
type ParticipationHistoryView { list(first: Int = 100, after: String, sort: CreatedAtSortInput): ParticipationPage! find(id: ID!): ParticipationDetail!}
type BusinessView { list( first: Int = 100 after: String sort: CreatedAtSortInput filter: InstanceFilterInput ): BusinessInstancePage! find(id: ID!): BusinessInstanceDetail!}
type OperationsView { list( first: Int = 100 after: String sort: CreatedAtSortInput filter: InstanceFilterInput ): OperationsPage! find(id: ID!): OperationsDetail!}
type AuditView { list( first: Int = 100 after: String sort: CreatedAtSortInput filter: InstanceFilterInput ): AuditPage! find(id: ID!): AuditDetail!}
type DeadLettersView { list(instanceId: ID!): [DeadLetterSummary!]!}
type Query { startCatalog: StartCatalogView! initiationHistory: InitiationHistoryView! assignedDialogs: AssignedDialogsView! participationHistory: ParticipationHistoryView! business: BusinessView! operations: OperationsView! audit: AuditView! deadLetters: DeadLettersView!}Reads return only what the signed-in account may see. The access check is part of the same database query as the filter and the cursor, and it runs before sorting and the page limit, so you cannot page or sort your way to a record you may not see. A view the account has no access to returns an empty list.
find returns NOT_FOUND with Resource not found both for a record that
does not exist and for one the account may not see.
Every command reads the account’s principals fresh from its provider, then
decides under the instance’s lock whether the account may act. If the
provider cannot answer, the command is refused. completeDialog needs one of
the dialog’s assignee principals; an access function alone is not enough.
The audit view’s evidence lists, for each recorded action, who acted, why,
and on which authority. Audit trail explains what it
contains.
| Mutation | Input | Requires | Returns |
|---|---|---|---|
startWorkflow | workflow (the id from startCatalog), optional version, releaseChannel, params, label, description, recipients | starter for the Workflow Definition; params must match the definition’s schema | InitiationDetail |
completeDialog | dialogId, status (COMPLETED or ERRORED), data | an assignee principal of the dialog; dialog pending; instance active or paused | DialogCompletionResult |
pauseInstance | instanceId, optional until, optional reason | operator; instance active | OperationsDetail |
resumeInstance | instanceId, optional reason | operator; instance paused | OperationsDetail |
terminateInstance | instanceId, optional reason | operator; instance active or paused | OperationsDetail |
reassignDialog | dialogId, assignees as a list of { providerId, subject }, reason | operator; dialog pending; instance active or paused | ReassignmentResult |
replayDeadLetter | deadLetterId, expectedRevision, reason | operator; the entry unchanged since you read it; step pending; instance active or paused | ReplayResult |
startWorkflow inputs:
workflowmust be theidof astartCatalogitem. A workflow name is rejected withBAD_USER_INPUTInvalid workflow.paramsare the start parameters. They are checked against the definition’s schema before the instance is created; parameters that do not match create nothing.labelanddescriptionare stored with the instance and shown with it.- Leave out
versionandreleaseChannel: the catalog ID already identifies a deployed definition. If supplied, they must match that definition. recipientsis a list of subjects the instance is for, stored with the instance. A principal relation registered in a hook receives them and returns the principals that access rules withprincipalRelation(name)grant; see Access control.
completeDialog with status: COMPLETED checks the answer against the
dialog’s schema, custom checks included, before it records it. An answer that
does not match is refused with BAD_USER_INPUT and one { path, message } per
rejected field in extensions.issues; nothing is recorded, and the dialog keeps
waiting for a corrected answer. With status: ERRORED, pass the error in
data: the dialog step fails with it, and the step’s retry policy decides what
happens next.
Operate running workflows describes pause, resume, terminate, reassignment and replay as operators use them.
input StartWorkflowInput { workflow: ID! version: String releaseChannel: String params: JSON label: String description: String recipients: [String!]}
enum DialogOutcomeStatus { COMPLETED ERRORED}
input CompleteDialogInput { dialogId: ID! status: DialogOutcomeStatus! data: JSON}
input PauseInstanceInput { instanceId: ID! until: DateTime reason: String}
input InstanceReasonInput { instanceId: ID! reason: String}
input AssigneeInput { providerId: String! subject: String!}
input ReassignDialogInput { dialogId: ID! assignees: [AssigneeInput!]! reason: String!}
input ReplayDeadLetterInput { deadLetterId: ID! expectedRevision: Int! reason: String!}
type DialogCompletionResult { dialogId: ID! instanceId: ID! status: DialogOutcomeStatus! decidedAt: DateTime!}
type ReassignmentResult { dialogId: ID! instanceId: ID! assigneeCount: Int! reassignedAt: DateTime!}
type ReplayResult { deadLetterId: ID! instanceId: ID! stepId: ID! replayedAt: DateTime!}
type Mutation { startWorkflow(input: StartWorkflowInput!): InitiationDetail! completeDialog(input: CompleteDialogInput!): DialogCompletionResult! pauseInstance(input: PauseInstanceInput!): OperationsDetail! resumeInstance(input: InstanceReasonInput!): OperationsDetail! terminateInstance(input: InstanceReasonInput!): OperationsDetail! reassignDialog(input: ReassignDialogInput!): ReassignmentResult! replayDeadLetter(input: ReplayDeadLetterInput!): ReplayResult!}IdentityFlow answers with HTTP 200 when a request reaches GraphQL
execution, errors included. An error is an entry in errors[] with message,
path and extensions.code. When a field the response needs fails, data
is null. A body that is not valid JSON gets 400 (BAD_REQUEST,
POST body sent invalid JSON.), methods other than GET and POST,
including OPTIONS, get 405, and a request whose Accept header asks for
application/graphql-response+json gets 400 for query validation errors.
extensions.code | message | When |
|---|---|---|
UNAUTHENTICATED | Authentication required | No session cookie, or an expired or unknown one. |
NOT_FOUND | Resource not found | find on a record that does not exist or that the account may not see. A command whose target does not exist or that the account may not see, or whose access could not be checked because the provider did not report the account’s principals. |
FORBIDDEN | Command not allowed | pauseInstance, resumeInstance or terminateInstance on an instance the account can see but may not operate. |
BAD_USER_INPUT | Invalid dialogId and similar, first must be between 1 and 100, Invalid pagination cursor, Input was rejected | A malformed ID, first out of range, a malformed cursor or filter, or command input that was rejected, such as a dialog answer that fails its schema. Input was rejected lists each rejected value in extensions.issues. |
CONFLICT | Resource state changed | The record changed since you read it, or is not in a state the command accepts. Reload and try again. |
LOCKED | Resource is temporarily locked | The record is briefly locked; a retry later may succeed. |
BAD_REQUEST | Must provide query string., POST body sent invalid JSON. | A request without a query, or a body that is not valid JSON. |
GRAPHQL_PARSE_FAILED | For example Syntax Error: Token limit of 1000 exceeded. | The query is not valid GraphQL, or has more than 1000 tokens. |
GRAPHQL_VALIDATION_FAILED | For example Cannot query field "nope" on type "AssignedDialogsView". | The query does not match the schema, or asks for introspection. |
| none | For example Syntax Error: Aliases limit of 15 exceeded, found 16. | More than 20 levels, 15 aliases, 50 directives or a calculated cost of 5,000. |
INTERNAL_SERVER_ERROR | Unexpected error. | A server error. The program’s log at the same moment names it. |
startWorkflow, completeDialog, reassignDialog and replayDeadLetter
never return FORBIDDEN. Their access is checked on the workflow or the
instance rather than on the dialog or dead letter they name, so a refusal
reads exactly like a missing target: NOT_FOUND. For example,
completeDialog from an account that is not an assignee returns NOT_FOUND.
extensions.issues is a list of { path, message }, one per rejected value.
It covers only what you sent: the command’s own fields, and startWorkflow
parameters that the workflow’s input schema rejects. Messages for those
parameters come from that schema, so the workflow’s author writes them.
The result and input types that the views and mutations above use.
scalar DateTimescalar JSON
enum InstanceStatus { ACTIVE PAUSED TERMINATED COMPLETED ERRORED}
enum SortDirection { ASC DESC}
enum WorkflowSortField { CREATED_AT NAME}
input WorkflowSortInput { field: WorkflowSortField! direction: SortDirection!}
input CreatedAtSortInput { direction: SortDirection!}
input WorkflowFilterInput { names: [String!] releaseChannels: [String!]}
input InstanceFilterInput { statuses: [InstanceStatus!] workflowNames: [String!]}
type PageInfo { endCursor: String hasNextPage: Boolean!}
type StartableWorkflowSummary { id: ID! name: String! version: String! releaseChannel: String! label: String createdAt: DateTime!}
type StartableWorkflow { id: ID! name: String! version: String! releaseChannel: String! label: String createdAt: DateTime! description: String inputContract: JSON}
type StartCatalogPage { items: [StartableWorkflowSummary!]! pageInfo: PageInfo!}
type InitiationSummary { id: ID! workflowId: ID! workflowName: String! workflowVersion: String! workflowReleaseChannel: String! label: String status: InstanceStatus! createdAt: DateTime! updatedAt: DateTime!}
type InitiationDetail { id: ID! workflowId: ID! workflowName: String! workflowVersion: String! workflowReleaseChannel: String! label: String status: InstanceStatus! createdAt: DateTime! updatedAt: DateTime! input: JSON! result: JSON!}
type InitiationPage { items: [InitiationSummary!]! pageInfo: PageInfo!}
type DialogSummary { id: ID! instanceId: ID! workflowName: String! name: String! message: String dueAt: DateTime createdAt: DateTime!}
type DialogDetail { id: ID! instanceId: ID! workflowName: String! name: String! message: String dueAt: DateTime createdAt: DateTime! taskContext: JSON!}
type DialogPage { items: [DialogSummary!]! pageInfo: PageInfo!}
type ParticipationSummary { id: ID! instanceId: ID! workflowName: String! name: String! decidedAt: DateTime!}
type ParticipationDetail { id: ID! instanceId: ID! workflowName: String! name: String! decidedAt: DateTime! taskContext: JSON! decision: JSON!}
type ParticipationPage { items: [ParticipationSummary!]! pageInfo: PageInfo!}
type BusinessInstanceSummary { id: ID! workflowId: ID! workflowName: String! workflowVersion: String! workflowReleaseChannel: String! label: String status: InstanceStatus! createdAt: DateTime! updatedAt: DateTime!}
type BusinessStepSummary { id: ID! name: String! kind: String! status: String! createdAt: DateTime! finishedAt: DateTime}
type BusinessInstanceDetail { id: ID! workflowId: ID! workflowName: String! workflowVersion: String! workflowReleaseChannel: String! label: String status: InstanceStatus! createdAt: DateTime! updatedAt: DateTime! description: String businessData: JSON steps: [BusinessStepSummary!]!}
type BusinessInstancePage { items: [BusinessInstanceSummary!]! pageInfo: PageInfo!}
type OperationsSummary { id: ID! workflowId: ID! workflowName: String! workflowVersion: String! workflowReleaseChannel: String! status: InstanceStatus! createdAt: DateTime! updatedAt: DateTime!}
type OperationsDiagnostics { stepCount: Int! erroredStepCount: Int!}
type OperationsDetail { id: ID! workflowId: ID! workflowName: String! workflowVersion: String! workflowReleaseChannel: String! status: InstanceStatus! createdAt: DateTime! updatedAt: DateTime! message: String until: DateTime finishedAt: DateTime diagnostics: OperationsDiagnostics!}
type OperationsPage { items: [OperationsSummary!]! pageInfo: PageInfo!}
type AuditSummary { id: ID! workflowId: ID! workflowName: String! workflowVersion: String! workflowReleaseChannel: String! status: InstanceStatus! createdAt: DateTime! updatedAt: DateTime! eventCount: Int!}
type PrincipalReference { providerId: String! subject: String!}
type AuditAuthority { kind: String! providerId: String matchedPrincipal: PrincipalReference policyVersion: String}
type AuditResource { kind: String! id: ID!}
type AuditEvidence { id: ID! action: String! resource: AuditResource! at: DateTime! actorAccountId: ID reason: String! result: String! authority: AuditAuthority!}
type AuditDetail { id: ID! workflowId: ID! workflowName: String! workflowVersion: String! workflowReleaseChannel: String! status: InstanceStatus! createdAt: DateTime! updatedAt: DateTime! eventCount: Int! evidence: [AuditEvidence!]!}
type AuditPage { items: [AuditSummary!]! pageInfo: PageInfo!}
type DeadLetterSummary { deadLetterId: ID! instanceId: ID! stepId: ID! revision: Int! failedAt: DateTime! failureCode: String! attempts: Int!}Calling other GraphQL services from a workflow
Section titled “Calling other GraphQL services from a workflow”A workflow calls another GraphQL service through a binding, not through this API; see Bindings and connectors.