Skip to content

GraphQL API

Signed-in browser same origin GraphQL API reads and commands Access checks current account
Signed-in browser same origin GraphQL API reads and commands Access checks current account
A browser UI uses the signed-in session; each read and command checks what that account may see or do.

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.

RootReturnsAccessfind returns
startCatalogWorkflow Definitions the account may startstarterStartableWorkflow, with description and inputContract
initiationHistoryInstances the account startedthe account started themInitiationDetail, with input and result
assignedDialogsPending dialogs the account may answerone of the account’s principals is an assigneeDialogDetail, with taskContext
participationHistoryDialogs the account answered, taken from the steps it finishedthe account answered themParticipationDetail, with taskContext and decision
businessInstances with their business dataviewerBusinessInstanceDetail, with businessData and steps
operationsInstances with their operational stateoperatorOperationsDetail, with message, until, diagnostics
auditInstances with their recorded evidenceauditorAuditDetail, with eventCount and evidence
deadLettersFailed assignee source jobs of one instance, via list(instanceId)operatorno 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. 1 to 100, default 100
  2. previous endCursor
Argument or fieldNotes
first1 to 100, default 100. Another value returns BAD_USER_INPUT first must be between 1 and 100.
afterThe 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.
sortInstance 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.
filternames and releaseChannels on startCatalog; statuses and workflowNames on the instance views. assignedDialogs and participationHistory have no filter.
itemsThe records of this page, in the order you asked for.
pageInfohasNextPage 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.

ONE DATABASE QUERY All records of the view Visible records access + filter Page first ≤ 100 keeps visible sorts, cuts
ONE DATABASE QUERY All records of the view Visible records access + filter Page first ≤ 100 keeps visible sorts, cuts
A list read checks access before it sorts and cuts the page, so a page holds only records the account may see.

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.

MutationInputRequiresReturns
startWorkflowworkflow (the id from startCatalog), optional version, releaseChannel, params, label, description, recipientsstarter for the Workflow Definition; params must match the definition’s schemaInitiationDetail
completeDialogdialogId, status (COMPLETED or ERRORED), dataan assignee principal of the dialog; dialog pending; instance active or pausedDialogCompletionResult
pauseInstanceinstanceId, optional until, optional reasonoperator; instance activeOperationsDetail
resumeInstanceinstanceId, optional reasonoperator; instance pausedOperationsDetail
terminateInstanceinstanceId, optional reasonoperator; instance active or pausedOperationsDetail
reassignDialogdialogId, assignees as a list of { providerId, subject }, reasonoperator; dialog pending; instance active or pausedReassignmentResult
replayDeadLetterdeadLetterId, expectedRevision, reasonoperator; the entry unchanged since you read it; step pending; instance active or pausedReplayResult

startWorkflow inputs:

  • workflow must be the id of a startCatalog item. A workflow name is rejected with BAD_USER_INPUT Invalid workflow.
  • params are the start parameters. They are checked against the definition’s schema before the instance is created; parameters that do not match create nothing.
  • label and description are stored with the instance and shown with it.
  • Leave out version and releaseChannel: the catalog ID already identifies a deployed definition. If supplied, they must match that definition.
  • recipients is 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 with principalRelation(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.

completeDialog status COMPLETED Schema check dialog schema Answer recorded returns COMPLETED BAD_USER_INPUT nothing recorded Dialog waits PENDING checks valid invalid leaves
completeDialog status COMPLETED Schema check dialog schema Answer recorded returns COMPLETED BAD_USER_INPUT nothing recorded Dialog waits PENDING checks valid invalid leaves
completeDialog checks the answer against the dialog’s schema before it records it; a refused answer leaves the dialog waiting.

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.codemessageWhen
UNAUTHENTICATEDAuthentication requiredNo session cookie, or an expired or unknown one.
NOT_FOUNDResource not foundfind 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.
FORBIDDENCommand not allowedpauseInstance, resumeInstance or terminateInstance on an instance the account can see but may not operate.
BAD_USER_INPUTInvalid dialogId and similar, first must be between 1 and 100, Invalid pagination cursor, Input was rejectedA 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.
CONFLICTResource state changedThe record changed since you read it, or is not in a state the command accepts. Reload and try again.
LOCKEDResource is temporarily lockedThe record is briefly locked; a retry later may succeed.
BAD_REQUESTMust provide query string., POST body sent invalid JSON.A request without a query, or a body that is not valid JSON.
GRAPHQL_PARSE_FAILEDFor example Syntax Error: Token limit of 1000 exceeded.The query is not valid GraphQL, or has more than 1000 tokens.
GRAPHQL_VALIDATION_FAILEDFor example Cannot query field "nope" on type "AssignedDialogsView".The query does not match the schema, or asks for introspection.
noneFor 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_ERRORUnexpected 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 DateTime
scalar 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!
}

A workflow calls another GraphQL service through a binding, not through this API; see Bindings and connectors.