{
"components": {
"schemas": {
"ApiKeyResponse": {
"description": "API key summary (never includes the hash or raw key).",
"properties": {
"created_at": {
"description": "Creation date.",
"format": "date-time",
"type": "string"
},
"expires_at": {
"description": "Expiration date.",
"format": "date-time",
"nullable": true,
"type": "string"
},
"id": {
"description": "API key ID.",
"format": "uuid",
"type": "string"
},
"is_active": {
"description": "Whether the key is active.",
"type": "boolean"
},
"key_prefix": {
"description": "First characters for identification.",
"type": "string"
},
"last_used_at": {
"description": "Last used date.",
"format": "date-time",
"nullable": true,
"type": "string"
},
"name": {
"description": "Key name.",
"type": "string"
},
"rate_limit_override": {
"description": "Per-key rate limit override (requests per minute), if set.",
"format": "int32",
"minimum": 0,
"nullable": true,
"type": "integer"
},
"scopes": {
"description": "Granted scopes.",
"items": {
"$ref": "#/components/schemas/ApiKeyScope"
},
"type": "array"
}
},
"required": [
"id",
"name",
"key_prefix",
"scopes",
"is_active",
"created_at"
],
"type": "object"
},
"ApiKeyScope": {
"description": "Permission scope for an API key.\n\nEach scope grants access to a specific set of actions.\nA key with no scopes has no permissions.",
"enum": [
"workflows_read",
"runs_read",
"runs_write",
"runs_manage",
"stats_read",
"admin"
],
"type": "string"
},
"ApprovalGrantedEvent": {
"description": "Payload of the `Event::ApprovalGranted` event.\n\n# Examples\n\n```\nuse chrono::Utc;\nuse ironflow_engine::notify::ApprovalGrantedEvent;\nuse uuid::Uuid;\n\nlet payload = ApprovalGrantedEvent {\n run_id: Uuid::now_v7(),\n approved_by: \"alice\".to_string(),\n at: Utc::now(),\n};\nassert_eq!(payload.approved_by, \"alice\");\n```",
"properties": {
"approved_by": {
"description": "User who approved (ID or username).",
"type": "string"
},
"at": {
"description": "When the approval was granted.",
"format": "date-time",
"type": "string"
},
"run_id": {
"description": "Run identifier.",
"format": "uuid",
"type": "string"
}
},
"required": [
"run_id",
"approved_by",
"at"
],
"type": "object"
},
"ApprovalRejectedEvent": {
"description": "Payload of the `Event::ApprovalRejected` event.\n\n# Examples\n\n```\nuse chrono::Utc;\nuse ironflow_engine::notify::ApprovalRejectedEvent;\nuse uuid::Uuid;\n\nlet payload = ApprovalRejectedEvent {\n run_id: Uuid::now_v7(),\n rejected_by: \"bob\".to_string(),\n at: Utc::now(),\n};\nassert_eq!(payload.rejected_by, \"bob\");\n```",
"properties": {
"at": {
"description": "When the rejection occurred.",
"format": "date-time",
"type": "string"
},
"rejected_by": {
"description": "User who rejected (ID or username).",
"type": "string"
},
"run_id": {
"description": "Run identifier.",
"format": "uuid",
"type": "string"
}
},
"required": [
"run_id",
"rejected_by",
"at"
],
"type": "object"
},
"ApprovalRequestedEvent": {
"description": "Payload of the `Event::ApprovalRequested` event.\n\n# Examples\n\n```\nuse chrono::Utc;\nuse ironflow_engine::notify::ApprovalRequestedEvent;\nuse uuid::Uuid;\n\nlet payload = ApprovalRequestedEvent {\n run_id: Uuid::now_v7(),\n step_id: Uuid::now_v7(),\n message: \"Deploy to prod?\".to_string(),\n at: Utc::now(),\n};\nassert_eq!(payload.message, \"Deploy to prod?\");\n```",
"properties": {
"at": {
"description": "When the approval was requested.",
"format": "date-time",
"type": "string"
},
"message": {
"description": "Message displayed to reviewers.",
"type": "string"
},
"run_id": {
"description": "Run identifier.",
"format": "uuid",
"type": "string"
},
"step_id": {
"description": "Approval step identifier.",
"format": "uuid",
"type": "string"
}
},
"required": [
"run_id",
"step_id",
"message",
"at"
],
"type": "object"
},
"ArtifactResponse": {
"description": "An artifact as exposed by the REST API.\n\nThe storage key is deliberately absent: it is internal plumbing, and callers\naddress an artifact by `(run, step, name)` through the download route.\n\n# Examples\n\n```\nuse chrono::Utc;\nuse ironflow_api::entities::ArtifactResponse;\nuse uuid::Uuid;\n\nlet response = ArtifactResponse {\n id: Uuid::now_v7(),\n step_id: Uuid::now_v7(),\n name: \"report.html\".to_string(),\n content_type: \"text/html\".to_string(),\n size_bytes: 142,\n sha256: \"0\".repeat(64),\n created_at: Utc::now(),\n};\nassert_eq!(response.name, \"report.html\");\n```",
"properties": {
"content_type": {
"description": "MIME type served on download.",
"type": "string"
},
"created_at": {
"description": "When the artifact was recorded.",
"format": "date-time",
"type": "string"
},
"id": {
"description": "Unique artifact identifier.",
"format": "uuid",
"type": "string"
},
"name": {
"description": "File name, unique within the step.",
"type": "string"
},
"sha256": {
"description": "Lowercase hex SHA-256 of the content.",
"type": "string"
},
"size_bytes": {
"description": "Size of the content, in bytes.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"step_id": {
"description": "The step that produced it.",
"format": "uuid",
"type": "string"
}
},
"required": [
"id",
"step_id",
"name",
"content_type",
"size_bytes",
"sha256",
"created_at"
],
"type": "object"
},
"AuditLogEntry": {
"description": "A persisted audit log entry capturing a domain event.\n\nEach entry records the full event payload as JSON alongside\ndenormalized contextual IDs for efficient filtering.\n\n# Examples\n\n```\nuse ironflow_store::entities::{AuditLogEntry, EventKind};\nuse uuid::Uuid;\nuse chrono::Utc;\nuse serde_json::json;\n\nlet entry = AuditLogEntry {\n id: Uuid::now_v7(),\n event_type: EventKind::RunStatusChanged,\n payload: json!({\"run_id\": \"...\"}),\n run_id: Some(Uuid::now_v7()),\n step_id: None,\n user_id: None,\n created_at: Utc::now(),\n};\nassert_eq!(entry.event_type, EventKind::RunStatusChanged);\n```",
"properties": {
"created_at": {
"description": "When the event was recorded.",
"format": "date-time",
"type": "string"
},
"event_type": {
"$ref": "#/components/schemas/EventKind",
"description": "Event type."
},
"id": {
"description": "Unique entry ID (UUID v7).",
"format": "uuid",
"type": "string"
},
"payload": {
"description": "Full serialized event payload."
},
"run_id": {
"description": "Associated run ID, if the event is run-scoped.",
"format": "uuid",
"nullable": true,
"type": "string"
},
"step_id": {
"description": "Associated step ID, if the event is step-scoped.",
"format": "uuid",
"nullable": true,
"type": "string"
},
"user_id": {
"description": "Associated user ID, if the event is user-scoped.",
"format": "uuid",
"nullable": true,
"type": "string"
}
},
"required": [
"id",
"event_type",
"payload",
"created_at"
],
"type": "object"
},
"ChangePasswordRequest": {
"description": "Change password request body.",
"properties": {
"new_password": {
"description": "New password (min 8 characters).",
"type": "string"
},
"old_password": {
"description": "Current password.",
"type": "string"
}
},
"required": [
"old_password",
"new_password"
],
"type": "object"
},
"CreateApiKeyRequest": {
"description": "Request body for creating an API key.",
"properties": {
"expires_at": {
"description": "Optional expiration date (ISO 8601).",
"format": "date-time",
"nullable": true,
"type": "string"
},
"name": {
"description": "Human-readable name for this key.",
"type": "string"
},
"rate_limit_override": {
"description": "Optional per-key rate limit override (requests per minute).\nWhen set, the server uses this value instead of the global rate\nlimit for requests authenticated with this key.",
"format": "int32",
"minimum": 0,
"nullable": true,
"type": "integer"
},
"scopes": {
"description": "Scopes to grant.",
"items": {
"$ref": "#/components/schemas/ApiKeyScope"
},
"type": "array"
}
},
"required": [
"name",
"scopes"
],
"type": "object"
},
"CreateApiKeyResponse": {
"description": "Response returned when creating an API key.\nThe raw key is only shown once.",
"properties": {
"created_at": {
"description": "Creation date.",
"format": "date-time",
"type": "string"
},
"expires_at": {
"description": "Expiration date.",
"format": "date-time",
"nullable": true,
"type": "string"
},
"id": {
"description": "API key ID.",
"format": "uuid",
"type": "string"
},
"key": {
"description": "The full raw API key (only returned at creation time).",
"type": "string"
},
"key_prefix": {
"description": "First characters for identification.",
"type": "string"
},
"name": {
"description": "Key name.",
"type": "string"
},
"rate_limit_override": {
"description": "Per-key rate limit override (requests per minute), if set.",
"format": "int32",
"minimum": 0,
"nullable": true,
"type": "integer"
},
"scopes": {
"description": "Granted scopes.",
"items": {
"$ref": "#/components/schemas/ApiKeyScope"
},
"type": "array"
}
},
"required": [
"id",
"key",
"key_prefix",
"name",
"scopes",
"created_at"
],
"type": "object"
},
"CreateRunRequest": {
"description": "Request to trigger a workflow.\n\n# Examples\n\n```\nuse ironflow_api::entities::CreateRunRequest;\nuse serde_json::json;\n\nlet req = CreateRunRequest {\n workflow: \"deploy\".to_string(),\n payload: Some(json!({\"env\": \"prod\"})),\n labels: None,\n scheduled_at: None,\n max_retries: Some(2),\n max_cost_usd: None,\n};\nassert_eq!(req.workflow, \"deploy\");\n```",
"properties": {
"labels": {
"additionalProperties": {
"type": "string"
},
"description": "Optional key-value labels for categorization and filtering.",
"nullable": true,
"propertyNames": {
"type": "string"
},
"type": "object"
},
"max_cost_usd": {
"description": "Optional cumulative cost cap for this run, in USD.\n\nOverrides the workflow default and the server default. `None` falls back\nto those. Must be zero or positive.",
"format": "double",
"nullable": true,
"type": "number"
},
"max_retries": {
"description": "How many times the run may be replayed automatically after a transient\nfailure. Defaults to `0`, meaning no automatic retry.\n\nEach retry waits an exponential backoff (30 s, 2 min, 8 min, capped at\n15 min) before the run is replayed from the start. Failures that cannot\nsucceed on replay -- an unknown workflow, an invalid payload, an\nexhausted agent budget, a rejected approval, a manual cancellation --\nconsume no attempt.",
"format": "int32",
"minimum": 0,
"nullable": true,
"type": "integer"
},
"payload": {
"additionalProperties": {},
"description": "Optional input payload for the workflow.",
"nullable": true,
"propertyNames": {
"type": "string"
},
"type": "object"
},
"scheduled_at": {
"description": "Optional deferred execution time. `None` means run immediately.",
"format": "date-time",
"nullable": true,
"type": "string"
},
"workflow": {
"description": "The workflow name to trigger.",
"type": "string"
}
},
"required": [
"workflow"
],
"type": "object"
},
"CreateScheduleRequest": {
"description": "Create schedule request body.",
"properties": {
"cron_expression": {
"description": "Cron expression (6-field format, e.g. `\"0 */5 * * * *\"`).",
"type": "string"
},
"inputs": {
"description": "JSON payload for the workflow. Defaults to `{}`."
},
"workflow_name": {
"description": "Name of the workflow to trigger.",
"type": "string"
}
},
"required": [
"workflow_name",
"cron_expression"
],
"type": "object"
},
"CreateUserRequest": {
"description": "Request body for creating a user (admin only).",
"properties": {
"email": {
"description": "Email address.",
"type": "string"
},
"is_admin": {
"description": "Whether the new user should be an admin.",
"type": "boolean"
},
"password": {
"description": "Plaintext password (min 8 characters).",
"type": "string"
},
"username": {
"description": "Display username.",
"type": "string"
}
},
"required": [
"email",
"username",
"password",
"is_admin"
],
"type": "object"
},
"CreatedBy": {
"description": "The author of a run, as exposed by the API.\n\nAlways present on a [`RunResponse`](crate::entities::RunResponse): a run with\nno recorded author (including every run created before authorship tracking)\nreports [`CreatedByKind::System`] with a label derived from its trigger, so\nclients never have to handle a missing value.\n\n# Examples\n\n```\nuse ironflow_api::entities::{CreatedBy, CreatedByKind};\n\n// Built from a Run via `CreatedBy::from(&run)`.\nlet system = CreatedBy {\n kind: CreatedByKind::System,\n id: None,\n label: \"manual\".to_string(),\n};\nassert_eq!(system.id, None);\n```",
"properties": {
"id": {
"description": "Identifier of the principal: the user ID, the API key ID, or `None` for\n[`CreatedByKind::System`].",
"format": "uuid",
"nullable": true,
"type": "string"
},
"kind": {
"$ref": "#/components/schemas/CreatedByKind",
"description": "What kind of principal triggered the run."
},
"label": {
"description": "Human-readable label. Never empty.\n\nThe username for a user, `\"key-name (username)\"` for an API key, and the\ntrigger description for a system run.",
"type": "string"
}
},
"required": [
"kind",
"label"
],
"type": "object"
},
"CreatedByKind": {
"description": "What kind of principal triggered a run.",
"enum": [
"user",
"api_key",
"system"
],
"type": "string"
},
"Event": {
"description": "A domain event emitted by the ironflow system.\n\nCovers the full lifecycle: runs, steps, approvals, and authentication.\nSubscribers receive these via [`EventPublisher`](super::EventPublisher)\nand pattern-match on the variants they care about.\n\nEach variant wraps a dedicated payload struct. The serialized form stays\nflat: the `type` discriminant sits next to the payload fields, so\n`{\"type\":\"run_created\",\"run_id\":...}` round-trips unchanged.\n\n# Examples\n\n```\nuse std::collections::HashMap;\nuse ironflow_engine::notify::{Event, RunStatusChangedEvent};\nuse ironflow_store::models::RunStatus;\nuse uuid::Uuid;\n\nlet event = Event::RunStatusChanged(RunStatusChangedEvent {\n run_id: Uuid::now_v7(),\n workflow_name: \"deploy\".to_string(),\n from: RunStatus::Running,\n to: RunStatus::Completed,\n error: None,\n cost_usd: rust_decimal::Decimal::ZERO,\n duration_ms: 5000,\n labels: HashMap::new(),\n at: chrono::Utc::now(),\n});\nassert_eq!(event.event_type(), \"run_status_changed\");\n```",
"oneOf": [
{
"allOf": [
{
"$ref": "#/components/schemas/RunCreatedEvent",
"description": "A new run was created (status: Pending)."
},
{
"properties": {
"type": {
"enum": [
"run_created"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A new run was created (status: Pending)."
},
{
"allOf": [
{
"$ref": "#/components/schemas/RunStatusChangedEvent",
"description": "A run changed status."
},
{
"properties": {
"type": {
"enum": [
"run_status_changed"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A run changed status."
},
{
"allOf": [
{
"$ref": "#/components/schemas/RunFailedEvent",
"description": "A run transitioned to [`Failed`](ironflow_store::models::RunStatus::Failed).\n\nThis is a convenience event emitted alongside [`RunStatusChanged`](Event::RunStatusChanged)\nwhen the target status is `Failed`. Subscribe to this instead of\n`RUN_STATUS_CHANGED` when you only care about failures."
},
{
"properties": {
"type": {
"enum": [
"run_failed"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A run transitioned to [`Failed`](ironflow_store::models::RunStatus::Failed).\n\nThis is a convenience event emitted alongside [`RunStatusChanged`](Event::RunStatusChanged)\nwhen the target status is `Failed`. Subscribe to this instead of\n`RUN_STATUS_CHANGED` when you only care about failures."
},
{
"allOf": [
{
"$ref": "#/components/schemas/RunBudgetExceededEvent",
"description": "A run was stopped because it reached its cumulative cost cap.\n\nEmitted when the engine refuses an agent step that would cross the run's\n`max_cost_usd`. The run transitions to\n[`Cancelled`](ironflow_store::models::RunStatus::Cancelled) and the step\nis never launched, so the reported spend is what the run had already\nconsumed."
},
{
"properties": {
"type": {
"enum": [
"run_budget_exceeded"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A run was stopped because it reached its cumulative cost cap.\n\nEmitted when the engine refuses an agent step that would cross the run's\n`max_cost_usd`. The run transitions to\n[`Cancelled`](ironflow_store::models::RunStatus::Cancelled) and the step\nis never launched, so the reported spend is what the run had already\nconsumed."
},
{
"allOf": [
{
"$ref": "#/components/schemas/RetryForcedEvent",
"description": "A manual retry was forced despite a handler version mismatch.\n\nEmitted when a caller passes `force=true` on a retry where the\nhandler version differs from the run's recorded version. This is\nan audit event: it means the new code will execute on the old\npayload without the handler explicitly declaring compatibility."
},
{
"properties": {
"type": {
"enum": [
"retry_forced"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A manual retry was forced despite a handler version mismatch.\n\nEmitted when a caller passes `force=true` on a retry where the\nhandler version differs from the run's recorded version. This is\nan audit event: it means the new code will execute on the old\npayload without the handler explicitly declaring compatibility."
},
{
"allOf": [
{
"$ref": "#/components/schemas/StepCompletedEvent",
"description": "A step completed successfully."
},
{
"properties": {
"type": {
"enum": [
"step_completed"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A step completed successfully."
},
{
"allOf": [
{
"$ref": "#/components/schemas/StepFailedEvent",
"description": "A step failed."
},
{
"properties": {
"type": {
"enum": [
"step_failed"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A step failed."
},
{
"allOf": [
{
"$ref": "#/components/schemas/ApprovalRequestedEvent",
"description": "A run is waiting for human approval."
},
{
"properties": {
"type": {
"enum": [
"approval_requested"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A run is waiting for human approval."
},
{
"allOf": [
{
"$ref": "#/components/schemas/ApprovalGrantedEvent",
"description": "A run was approved by a human."
},
{
"properties": {
"type": {
"enum": [
"approval_granted"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A run was approved by a human."
},
{
"allOf": [
{
"$ref": "#/components/schemas/ApprovalRejectedEvent",
"description": "A run was rejected by a human."
},
{
"properties": {
"type": {
"enum": [
"approval_rejected"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A run was rejected by a human."
},
{
"allOf": [
{
"$ref": "#/components/schemas/LogLineEvent",
"description": "A log line emitted during step execution.\n\nPushed by the worker in real time so that SSE clients can stream\nstep output as it happens, without waiting for step completion."
},
{
"properties": {
"type": {
"enum": [
"log_line"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A log line emitted during step execution.\n\nPushed by the worker in real time so that SSE clients can stream\nstep output as it happens, without waiting for step completion."
},
{
"allOf": [
{
"$ref": "#/components/schemas/UserSignedInEvent",
"description": "A user signed in."
},
{
"properties": {
"type": {
"enum": [
"user_signed_in"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A user signed in."
},
{
"allOf": [
{
"$ref": "#/components/schemas/UserSignedUpEvent",
"description": "A new user signed up."
},
{
"properties": {
"type": {
"enum": [
"user_signed_up"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A new user signed up."
},
{
"allOf": [
{
"$ref": "#/components/schemas/UserSignedOutEvent",
"description": "A user signed out."
},
{
"properties": {
"type": {
"enum": [
"user_signed_out"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A user signed out."
}
]
},
"EventKind": {
"description": "Strongly-typed event kind matching domain event variants.\n\nSerializes to/from `snake_case` strings (e.g. `\"run_status_changed\"`).\n\n# Examples\n\n```\nuse ironflow_store::entities::EventKind;\n\nlet kind: EventKind = \"run_status_changed\".parse().unwrap();\nassert_eq!(kind, EventKind::RunStatusChanged);\nassert_eq!(kind.as_str(), \"run_status_changed\");\n```",
"enum": [
"run_created",
"run_status_changed",
"run_failed",
"run_budget_exceeded",
"step_completed",
"step_failed",
"approval_requested",
"approval_granted",
"approval_rejected",
"log_line",
"user_signed_in",
"user_signed_up",
"user_signed_out",
"secrets_rotated",
"retry_forced"
],
"type": "string"
},
"GetRunLogsQuery": {
"description": "Query parameters for listing run logs.",
"properties": {
"cursor": {
"description": "Cursor for pagination (last entry ID from previous page).",
"format": "uuid",
"nullable": true,
"type": "string"
},
"limit": {
"description": "Number of entries to return (default: 100, max: 1000).",
"format": "int32",
"minimum": 0,
"nullable": true,
"type": "integer"
},
"step_id": {
"description": "Filter by step ID.",
"format": "uuid",
"nullable": true,
"type": "string"
},
"stream": {
"$ref": "#/components/schemas/LogStream",
"description": "Filter by output stream (`stdout`, `stderr`, `system`).",
"nullable": true
}
},
"type": "object"
},
"HistoryGranularity": {
"description": "Time bucket granularity for historical statistics.\n\nControls the size of each time bucket in the response.\n\n# Examples\n\n```\nuse ironflow_store::entities::HistoryGranularity;\n\nlet gran = HistoryGranularity::OneDay;\nassert_eq!(gran.to_string(), \"1d\");\nassert_eq!(gran.pg_interval(), \"day\");\n```",
"enum": [
"1h",
"1d",
"1w"
],
"type": "string"
},
"HistoryPeriod": {
"description": "Time period for historical statistics queries.\n\nControls how far back the query reaches from the current time.\n\n# Examples\n\n```\nuse ironflow_store::entities::HistoryPeriod;\n\nlet period = HistoryPeriod::SevenDays;\nassert_eq!(period.to_string(), \"7d\");\n```",
"enum": [
"24h",
"7d",
"30d",
"90d"
],
"type": "string"
},
"KeyVersionsResponse": {
"description": "How the configured key ring lines up with the stored secrets.",
"properties": {
"active": {
"description": "Version used to encrypt new secrets.",
"format": "int32",
"type": "integer"
},
"configured": {
"description": "Versions present in the configured key ring.",
"items": {
"format": "int32",
"type": "integer"
},
"type": "array"
},
"in_use": {
"description": "Versions actually used by stored secrets.",
"items": {
"format": "int32",
"type": "integer"
},
"type": "array"
},
"missing": {
"description": "Versions used by stored secrets but absent from the key ring.\nNon-empty means some secrets are unreadable.",
"items": {
"format": "int32",
"type": "integer"
},
"type": "array"
},
"retirable": {
"description": "Versions that can be removed from the key ring safely.",
"items": {
"format": "int32",
"type": "integer"
},
"type": "array"
}
},
"required": [
"active",
"configured",
"in_use",
"missing",
"retirable"
],
"type": "object"
},
"ListAuditLogsQuery": {
"description": "Query parameters for listing audit log entries.",
"properties": {
"event_type": {
"$ref": "#/components/schemas/EventKind",
"description": "Filter by event type (e.g. `run_status_changed`).",
"nullable": true
},
"from": {
"description": "Filter entries created at or after this timestamp.",
"format": "date-time",
"nullable": true,
"type": "string"
},
"page": {
"description": "Page number (1-based, default: 1).",
"format": "int32",
"minimum": 0,
"nullable": true,
"type": "integer"
},
"per_page": {
"description": "Items per page (default: 50, max: 100).",
"format": "int32",
"minimum": 0,
"nullable": true,
"type": "integer"
},
"run_id": {
"description": "Filter by run ID.",
"format": "uuid",
"nullable": true,
"type": "string"
},
"to": {
"description": "Filter entries created at or before this timestamp.",
"format": "date-time",
"nullable": true,
"type": "string"
}
},
"type": "object"
},
"ListRunsQuery": {
"description": "Query parameters for listing runs.",
"properties": {
"created_by": {
"description": "Filter by author: the user ID that triggered the run.\n\nAlso matches runs triggered by one of that user's API keys.",
"format": "uuid",
"nullable": true,
"type": "string"
},
"has_steps": {
"description": "Filter by step presence (only applies to completed/cancelled runs).\nNon-terminal runs (pending, running, etc.) are always included.\nWhen `true`, only return completed/cancelled runs that have steps.\nWhen `false`, only return completed/cancelled runs without steps.",
"nullable": true,
"type": "boolean"
},
"label": {
"description": "Filter by labels. Comma-separated `key:value` pairs.",
"nullable": true,
"type": "string"
},
"page": {
"description": "Page number (1-based).",
"format": "int32",
"minimum": 0,
"nullable": true,
"type": "integer"
},
"per_page": {
"description": "Items per page.",
"format": "int32",
"minimum": 0,
"nullable": true,
"type": "integer"
},
"status": {
"$ref": "#/components/schemas/RunStatus",
"description": "Filter by run status.",
"nullable": true
},
"workflow": {
"description": "Filter by workflow name.",
"nullable": true,
"type": "string"
}
},
"type": "object"
},
"ListUsersQuery": {
"description": "Query parameters for listing users.",
"properties": {
"page": {
"description": "Page number (1-based, defaults to 1).",
"format": "int32",
"minimum": 0,
"nullable": true,
"type": "integer"
},
"per_page": {
"description": "Items per page (defaults to 20, max 100).",
"format": "int32",
"minimum": 0,
"nullable": true,
"type": "integer"
}
},
"type": "object"
},
"ListWorkflowsQuery": {
"description": "Query parameters for listing workflows.",
"properties": {
"category": {
"description": "Optional case-insensitive partial match on the category path\n(e.g. `etl` matches `Data/ETL` and `data/etl/nightly`).\n\nPass `__uncategorized__` to list only workflows without any\ncategory.",
"nullable": true,
"type": "string"
},
"name": {
"description": "Optional case-insensitive partial match on workflow name.",
"nullable": true,
"type": "string"
}
},
"type": "object"
},
"LogCursorMeta": {
"description": "Cursor-based pagination metadata for log entries.",
"properties": {
"has_more": {
"description": "Whether more entries exist after this page.",
"type": "boolean"
},
"next_cursor": {
"description": "Cursor to pass for the next page. `None` when there are no more entries.",
"format": "uuid",
"nullable": true,
"type": "string"
}
},
"required": [
"has_more"
],
"type": "object"
},
"LogEntry": {
"description": "A persisted log line from step execution.\n\nEach entry records a single line of output alongside its metadata.\nEntries are ordered by [`id`](LogEntry::id) (UUID v7, time-ordered).\n\n# Examples\n\n```\nuse ironflow_store::entities::{LogEntry, LogStream};\nuse uuid::Uuid;\nuse chrono::Utc;\n\nlet entry = LogEntry {\n id: Uuid::now_v7(),\n run_id: Uuid::now_v7(),\n step_id: Uuid::now_v7(),\n step_name: \"build\".to_string(),\n stream: LogStream::Stdout,\n line: \"Compiling ironflow v0.1.0\".to_string(),\n created_at: Utc::now(),\n};\nassert_eq!(entry.stream, LogStream::Stdout);\n```",
"properties": {
"created_at": {
"description": "When the line was recorded.",
"format": "date-time",
"type": "string"
},
"id": {
"description": "Unique entry ID (UUID v7, time-ordered).",
"format": "uuid",
"type": "string"
},
"line": {
"description": "The log line content.",
"type": "string"
},
"run_id": {
"description": "Run that produced this log line.",
"format": "uuid",
"type": "string"
},
"step_id": {
"description": "Step that produced this log line.",
"format": "uuid",
"type": "string"
},
"step_name": {
"description": "Human-readable step name.",
"type": "string"
},
"stream": {
"$ref": "#/components/schemas/LogStream",
"description": "Output stream."
}
},
"required": [
"id",
"run_id",
"step_id",
"step_name",
"stream",
"line",
"created_at"
],
"type": "object"
},
"LogLineEvent": {
"description": "Payload of the `Event::LogLine` event.\n\n# Examples\n\n```\nuse chrono::Utc;\nuse ironflow_engine::notify::{LogLineEvent, LogStream};\nuse uuid::Uuid;\n\nlet payload = LogLineEvent {\n run_id: Uuid::now_v7(),\n step_id: Uuid::now_v7(),\n step_name: \"build\".to_string(),\n stream: LogStream::Stdout,\n line: \"Compiling ironflow v0.1.0\".to_string(),\n at: Utc::now(),\n};\nassert_eq!(payload.line, \"Compiling ironflow v0.1.0\");\n```",
"properties": {
"at": {
"description": "When the line was emitted.",
"format": "date-time",
"type": "string"
},
"line": {
"description": "The log line content.",
"type": "string"
},
"run_id": {
"description": "Run identifier.",
"format": "uuid",
"type": "string"
},
"step_id": {
"description": "Step identifier.",
"format": "uuid",
"type": "string"
},
"step_name": {
"description": "Human-readable step name.",
"type": "string"
},
"stream": {
"$ref": "#/components/schemas/LogStream",
"description": "Output stream."
}
},
"required": [
"run_id",
"step_id",
"step_name",
"stream",
"line",
"at"
],
"type": "object"
},
"LogStream": {
"description": "Output stream for a log line.\n\n# Examples\n\n```\nuse ironflow_store::entities::LogStream;\n\nlet stream: LogStream = \"stdout\".parse().unwrap();\nassert_eq!(stream.as_str(), \"stdout\");\n```",
"enum": [
"stdout",
"stderr",
"system"
],
"type": "string"
},
"MeResponse": {
"description": "Current user profile response.",
"properties": {
"created_at": {
"description": "When the user account was created.",
"format": "date-time",
"type": "string"
},
"email": {
"description": "Email address.",
"type": "string"
},
"is_admin": {
"description": "Admin flag.",
"type": "boolean"
},
"user_id": {
"description": "User ID.",
"format": "uuid",
"type": "string"
},
"username": {
"description": "Display username.",
"type": "string"
}
},
"required": [
"user_id",
"email",
"username",
"is_admin",
"created_at"
],
"type": "object"
},
"RetryForcedEvent": {
"description": "Payload of the `Event::RetryForced` event.\n\n# Examples\n\n```\nuse chrono::Utc;\nuse ironflow_engine::notify::RetryForcedEvent;\nuse uuid::Uuid;\n\nlet payload = RetryForcedEvent {\n run_id: Uuid::now_v7(),\n workflow_name: \"deploy\".to_string(),\n original_version: \"1\".to_string(),\n current_version: \"2\".to_string(),\n at: Utc::now(),\n};\nassert_eq!(payload.current_version, \"2\");\n```",
"properties": {
"at": {
"description": "When the forced retry occurred.",
"format": "date-time",
"type": "string"
},
"current_version": {
"description": "Current version of the handler.",
"type": "string"
},
"original_version": {
"description": "Version stored on the original run.",
"type": "string"
},
"run_id": {
"description": "The new run created by the forced retry.",
"format": "uuid",
"type": "string"
},
"workflow_name": {
"description": "Workflow name.",
"type": "string"
}
},
"required": [
"run_id",
"workflow_name",
"original_version",
"current_version",
"at"
],
"type": "object"
},
"RotateSecretsRequest": {
"description": "Request body for rotating a batch of secrets to another key version.\n\nRotation is driven one batch per call so a long rotation never becomes a\nlong HTTP request: the client loops, carrying `after_id` forward.",
"properties": {
"after_id": {
"description": "Resume after this secret ID. Omit to start from the beginning.",
"format": "uuid",
"nullable": true,
"type": "string"
},
"batch_size": {
"description": "Secrets to process in this batch. Defaults to 100, clamped to 1000.",
"format": "int32",
"minimum": 0,
"nullable": true,
"type": "integer"
},
"to_version": {
"description": "Target key version. Defaults to the server's active version.",
"format": "int32",
"nullable": true,
"type": "integer"
}
},
"type": "object"
},
"RotateSecretsResponse": {
"description": "Outcome of one rotation batch.",
"properties": {
"failed": {
"description": "Secrets skipped because they could not be decrypted.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"last_id": {
"description": "Highest secret ID seen in this batch. Pass it back as `after_id` to\ncontinue; `null` means there is nothing left to do.",
"format": "uuid",
"nullable": true,
"type": "string"
},
"remaining": {
"description": "Secrets left on another key version after this batch.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"rotated": {
"description": "Secrets successfully re-encrypted in this batch.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"to_version": {
"description": "Key version the batch re-encrypted towards.",
"format": "int32",
"type": "integer"
}
},
"required": [
"to_version",
"rotated",
"failed",
"remaining"
],
"type": "object"
},
"RunBudgetExceededEvent": {
"description": "Payload of the `Event::RunBudgetExceeded` event.\n\n# Examples\n\n```\nuse chrono::Utc;\nuse ironflow_engine::notify::RunBudgetExceededEvent;\nuse rust_decimal::Decimal;\nuse uuid::Uuid;\n\nlet payload = RunBudgetExceededEvent {\n run_id: Uuid::now_v7(),\n workflow_name: \"deploy\".to_string(),\n limit_usd: Decimal::new(200, 2),\n spent_usd: Decimal::new(180, 2),\n step_budget_usd: Decimal::new(50, 2),\n at: Utc::now(),\n};\nassert_eq!(payload.limit_usd, Decimal::new(200, 2));\n```",
"properties": {
"at": {
"description": "When the refusal occurred.",
"format": "date-time",
"type": "string"
},
"limit_usd": {
"description": "The configured cost cap in USD.",
"format": "double",
"type": "number"
},
"run_id": {
"description": "Run identifier.",
"format": "uuid",
"type": "string"
},
"spent_usd": {
"description": "Cost already consumed when the cap was reached, in USD.",
"format": "double",
"type": "number"
},
"step_budget_usd": {
"description": "Declared budget of the refused step, in USD.",
"format": "double",
"type": "number"
},
"workflow_name": {
"description": "Workflow name.",
"type": "string"
}
},
"required": [
"run_id",
"workflow_name",
"limit_usd",
"spent_usd",
"step_budget_usd",
"at"
],
"type": "object"
},
"RunCreatedEvent": {
"description": "Payload of the `Event::RunCreated` event.\n\n# Examples\n\n```\nuse chrono::Utc;\nuse ironflow_engine::notify::RunCreatedEvent;\nuse uuid::Uuid;\n\nlet payload = RunCreatedEvent {\n run_id: Uuid::now_v7(),\n workflow_name: \"deploy\".to_string(),\n at: Utc::now(),\n};\nassert_eq!(payload.workflow_name, \"deploy\");\n```",
"properties": {
"at": {
"description": "When the run was created.",
"format": "date-time",
"type": "string"
},
"run_id": {
"description": "Run identifier.",
"format": "uuid",
"type": "string"
},
"workflow_name": {
"description": "Workflow name.",
"type": "string"
}
},
"required": [
"run_id",
"workflow_name",
"at"
],
"type": "object"
},
"RunDetailResponse": {
"description": "Run detail response — includes steps and payload.",
"properties": {
"payload": {
"description": "Input payload that triggered this run."
},
"run": {
"$ref": "#/components/schemas/RunResponse",
"description": "The run."
},
"steps": {
"description": "Associated steps, ordered by position.",
"items": {
"$ref": "#/components/schemas/StepResponse"
},
"type": "array"
}
},
"required": [
"run",
"steps",
"payload"
],
"type": "object"
},
"RunFailedEvent": {
"description": "Payload of the `Event::RunFailed` event.\n\n# Examples\n\n```\nuse std::collections::HashMap;\n\nuse chrono::Utc;\nuse ironflow_engine::notify::RunFailedEvent;\nuse rust_decimal::Decimal;\nuse uuid::Uuid;\n\nlet payload = RunFailedEvent {\n run_id: Uuid::now_v7(),\n workflow_name: \"deploy\".to_string(),\n error: Some(\"step crashed\".to_string()),\n cost_usd: Decimal::ZERO,\n duration_ms: 3000,\n labels: HashMap::new(),\n at: Utc::now(),\n};\nassert_eq!(payload.error.as_deref(), Some(\"step crashed\"));\n```",
"properties": {
"at": {
"description": "When the failure occurred.",
"format": "date-time",
"type": "string"
},
"cost_usd": {
"description": "Aggregated cost in USD at the time of failure.",
"format": "double",
"type": "number"
},
"duration_ms": {
"description": "Aggregated duration in milliseconds at the time of failure.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"error": {
"description": "Error message.",
"nullable": true,
"type": "string"
},
"labels": {
"additionalProperties": {
"type": "string"
},
"description": "Labels of the run at the time of the failure.",
"propertyNames": {
"type": "string"
},
"type": "object"
},
"run_id": {
"description": "Run identifier.",
"format": "uuid",
"type": "string"
},
"workflow_name": {
"description": "Workflow name.",
"type": "string"
}
},
"required": [
"run_id",
"workflow_name",
"cost_usd",
"duration_ms",
"at"
],
"type": "object"
},
"RunResponse": {
"description": "Run response DTO — public API representation of a run.\n\nMaps from the internal [`Run`] model, exposing only necessary fields.\n\n# Examples\n\n```\nuse ironflow_store::models::{Run, RunStatus, TriggerKind};\nuse ironflow_api::entities::RunResponse;\n```",
"properties": {
"completed_at": {
"description": "When execution completed.",
"format": "date-time",
"nullable": true,
"type": "string"
},
"cost_usd": {
"description": "Aggregated cost in USD.",
"format": "double",
"type": "number"
},
"created_at": {
"description": "When created.",
"format": "date-time",
"type": "string"
},
"created_by": {
"$ref": "#/components/schemas/CreatedBy",
"description": "Who triggered the run. Always present."
},
"duration_ms": {
"description": "Total duration in milliseconds.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"error": {
"description": "Optional error message.",
"nullable": true,
"type": "string"
},
"handler_version": {
"description": "Version of the handler that created this run.",
"nullable": true,
"type": "string"
},
"id": {
"description": "Unique run identifier.",
"format": "uuid",
"type": "string"
},
"idempotency_key": {
"description": "Idempotency key that produced this run, when one was supplied.",
"nullable": true,
"type": "string"
},
"labels": {
"additionalProperties": {
"type": "string"
},
"description": "User-defined key-value labels.",
"propertyNames": {
"type": "string"
},
"type": "object"
},
"max_cost_usd": {
"description": "Cumulative cost cap for this run, in USD. `None` means no cap.",
"format": "double",
"nullable": true,
"type": "number"
},
"max_retries": {
"description": "Maximum allowed retries.",
"format": "int32",
"minimum": 0,
"type": "integer"
},
"retry_count": {
"description": "Number of times retried.",
"format": "int32",
"minimum": 0,
"type": "integer"
},
"scheduled_at": {
"description": "Scheduled execution time. `None` means the run executed immediately.",
"format": "date-time",
"nullable": true,
"type": "string"
},
"started_at": {
"description": "When execution started.",
"format": "date-time",
"nullable": true,
"type": "string"
},
"status": {
"$ref": "#/components/schemas/RunStatus",
"description": "Current status."
},
"trigger": {
"$ref": "#/components/schemas/TriggerKind",
"description": "How the run was triggered."
},
"updated_at": {
"description": "When last updated.",
"format": "date-time",
"type": "string"
},
"workflow_name": {
"description": "Workflow name.",
"type": "string"
}
},
"required": [
"id",
"workflow_name",
"status",
"trigger",
"retry_count",
"max_retries",
"cost_usd",
"duration_ms",
"created_at",
"updated_at",
"created_by"
],
"type": "object"
},
"RunStatus": {
"description": "Status of a workflow run, forming a finite state machine.\n\nValid transitions:\n- `Pending` -> `Running`, `Cancelled`\n- `Running` -> `Pending` (worker lease expired), `Completed`, `Failed`, `Warning`, `Retrying`, `Cancelled`, `AwaitingApproval`, `Sleeping`\n- `Retrying` -> `Running`, `Failed`, `Cancelled`\n- `AwaitingApproval` -> `Running`, `Failed`, `Cancelled`\n- `Sleeping` -> `Pending` (wake-up timer elapsed), `Cancelled`\n\nTerminal states (`Completed`, `Failed`, `Warning`, `Cancelled`) are idempotent:\ntransitioning to the same terminal state is a no-op, not an error.\n\n# Examples\n\n```\nuse ironflow_store::entities::RunStatus;\n\nassert!(RunStatus::Pending.can_transition_to(&RunStatus::Running));\nassert!(!RunStatus::Pending.can_transition_to(&RunStatus::Completed));\nassert!(!RunStatus::Completed.can_transition_to(&RunStatus::Running));\nassert!(RunStatus::Running.can_transition_to(&RunStatus::AwaitingApproval));\nassert!(RunStatus::Running.can_transition_to(&RunStatus::Warning));\n// A run whose worker lease expired goes back to the queue:\nassert!(RunStatus::Running.can_transition_to(&RunStatus::Pending));\nassert!(RunStatus::AwaitingApproval.can_transition_to(&RunStatus::Running));\nassert!(RunStatus::Running.can_transition_to(&RunStatus::Sleeping));\nassert!(RunStatus::Sleeping.can_transition_to(&RunStatus::Pending));\nassert!(RunStatus::Sleeping.can_transition_to(&RunStatus::Cancelled));\n// Terminal-to-same is idempotent:\nassert!(RunStatus::Failed.can_transition_to(&RunStatus::Failed));\nassert!(RunStatus::Completed.can_transition_to(&RunStatus::Completed));\nassert!(RunStatus::Warning.can_transition_to(&RunStatus::Warning));\nassert!(RunStatus::Cancelled.can_transition_to(&RunStatus::Cancelled));\n```",
"enum": [
"pending",
"running",
"completed",
"failed",
"retrying",
"cancelled",
"awaiting_approval",
"warning",
"sleeping"
],
"type": "string"
},
"RunStatusChangedEvent": {
"description": "Payload of the `Event::RunStatusChanged` event.\n\n# Examples\n\n```\nuse std::collections::HashMap;\n\nuse chrono::Utc;\nuse ironflow_engine::notify::RunStatusChangedEvent;\nuse ironflow_store::models::RunStatus;\nuse rust_decimal::Decimal;\nuse uuid::Uuid;\n\nlet payload = RunStatusChangedEvent {\n run_id: Uuid::now_v7(),\n workflow_name: \"deploy\".to_string(),\n from: RunStatus::Running,\n to: RunStatus::Completed,\n error: None,\n cost_usd: Decimal::ZERO,\n duration_ms: 5000,\n labels: HashMap::new(),\n at: Utc::now(),\n};\nassert_eq!(payload.to, RunStatus::Completed);\n```",
"properties": {
"at": {
"description": "When the transition occurred.",
"format": "date-time",
"type": "string"
},
"cost_usd": {
"description": "Aggregated cost in USD at the time of transition.",
"format": "double",
"type": "number"
},
"duration_ms": {
"description": "Aggregated duration in milliseconds at the time of transition.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"error": {
"description": "Error message (when transitioning to Failed).",
"nullable": true,
"type": "string"
},
"from": {
"$ref": "#/components/schemas/RunStatus",
"description": "Previous status."
},
"labels": {
"additionalProperties": {
"type": "string"
},
"description": "Labels of the run at the time of the transition.",
"propertyNames": {
"type": "string"
},
"type": "object"
},
"run_id": {
"description": "Run identifier.",
"format": "uuid",
"type": "string"
},
"to": {
"$ref": "#/components/schemas/RunStatus",
"description": "New status."
},
"workflow_name": {
"description": "Workflow name.",
"type": "string"
}
},
"required": [
"run_id",
"workflow_name",
"from",
"to",
"cost_usd",
"duration_ms",
"at"
],
"type": "object"
},
"ScheduleResponse": {
"description": "Schedule list/detail response.",
"properties": {
"created_at": {
"description": "When the schedule was created.",
"format": "date-time",
"type": "string"
},
"created_by_user_id": {
"description": "User who created the schedule. `None` for handler-declared schedules.",
"format": "uuid",
"nullable": true,
"type": "string"
},
"cron_expression": {
"description": "Cron expression.",
"type": "string"
},
"disabled_at": {
"description": "When the schedule was disabled. `None` means active.",
"format": "date-time",
"nullable": true,
"type": "string"
},
"id": {
"description": "Schedule ID.",
"format": "uuid",
"type": "string"
},
"inputs": {
"description": "JSON payload passed to the workflow."
},
"last_triggered_at": {
"description": "When the schedule last created a run.",
"format": "date-time",
"nullable": true,
"type": "string"
},
"next_trigger_at": {
"description": "When the schedule will next fire.",
"format": "date-time",
"nullable": true,
"type": "string"
},
"source": {
"$ref": "#/components/schemas/ScheduleSource",
"description": "Where this schedule was created (`handler` or `api`)."
},
"updated_at": {
"description": "When the schedule was last updated.",
"format": "date-time",
"type": "string"
},
"workflow_name": {
"description": "Name of the workflow to trigger.",
"type": "string"
}
},
"required": [
"id",
"workflow_name",
"cron_expression",
"inputs",
"source",
"created_at",
"updated_at"
],
"type": "object"
},
"ScheduleSource": {
"description": "Where a schedule was created.\n\n`Handler` schedules are declared in code via [`WorkflowHandler::schedule()`]\nand synced to the database at startup. `Api` schedules are created by users\nthrough the REST API, CLI, or dashboard.\n\n# Examples\n\n```\nuse ironflow_store::entities::ScheduleSource;\n\nlet source = ScheduleSource::Handler;\nassert_eq!(source.as_str(), \"handler\");\n\nlet parsed: ScheduleSource = \"api\".parse().unwrap();\nassert_eq!(parsed, ScheduleSource::Api);\n```",
"enum": [
"handler",
"api"
],
"type": "string"
},
"ScopeEntry": {
"description": "A scope entry with its machine name and human-readable label.",
"properties": {
"description": {
"description": "Short description.",
"type": "string"
},
"label": {
"description": "Human-readable label (e.g. \"Runs Read\").",
"type": "string"
},
"value": {
"description": "Machine-readable scope value (e.g. \"runs_read\").",
"type": "string"
}
},
"required": [
"value",
"label",
"description"
],
"type": "object"
},
"SecretResponse": {
"description": "Response DTO for a secret (never exposes the value).",
"properties": {
"created_at": {
"description": "Creation timestamp.",
"format": "date-time",
"type": "string"
},
"id": {
"description": "Secret ID.",
"format": "uuid",
"type": "string"
},
"key": {
"description": "Secret key.",
"type": "string"
},
"updated_at": {
"description": "Last update timestamp.",
"format": "date-time",
"type": "string"
}
},
"required": [
"id",
"key",
"created_at",
"updated_at"
],
"type": "object"
},
"SetSecretRequest": {
"description": "Request body for creating or updating a secret.",
"properties": {
"key": {
"description": "Secret key (namespaced, e.g. `workflows/inbox/gmail_token`).",
"type": "string"
},
"value": {
"description": "Secret value (plaintext, will be encrypted at rest).",
"type": "string"
}
},
"required": [
"key",
"value"
],
"type": "object"
},
"SignInRequest": {
"description": "Sign-in request body.",
"properties": {
"email": {
"description": "Email address.",
"type": "string"
},
"password": {
"description": "Plaintext password.",
"type": "string"
}
},
"required": [
"email",
"password"
],
"type": "object"
},
"SignUpRequest": {
"description": "Sign-up request body.",
"properties": {
"email": {
"description": "Email address.",
"type": "string"
},
"password": {
"description": "Plaintext password (min 8 characters).",
"type": "string"
},
"username": {
"description": "Display username.",
"type": "string"
}
},
"required": [
"email",
"username",
"password"
],
"type": "object"
},
"StatsHistoryBucketResponse": {
"description": "One time bucket in the history response.\n\n# Examples\n\n```\nuse ironflow_api::entities::StatsHistoryBucketResponse;\n```",
"properties": {
"avg_duration_ms": {
"description": "Average duration in milliseconds.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"cancelled": {
"description": "Number of cancelled runs in this bucket.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"completed": {
"description": "Number of completed runs in this bucket.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"failed": {
"description": "Number of failed runs in this bucket.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"p95_duration_ms": {
"description": "95th percentile duration in milliseconds.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"time": {
"description": "Start of the time bucket.",
"format": "date-time",
"type": "string"
},
"total_cost_usd": {
"description": "Total cost in USD.",
"format": "double",
"type": "number"
}
},
"required": [
"time",
"completed",
"failed",
"cancelled",
"avg_duration_ms",
"p95_duration_ms",
"total_cost_usd"
],
"type": "object"
},
"StatsHistoryResponse": {
"description": "Time-bucketed historical statistics response.\n\n# Examples\n\n```\nuse ironflow_api::entities::StatsHistoryResponse;\n```",
"properties": {
"buckets": {
"description": "Aggregated buckets, sorted by time ascending.",
"items": {
"$ref": "#/components/schemas/StatsHistoryBucketResponse"
},
"type": "array"
},
"granularity": {
"$ref": "#/components/schemas/HistoryGranularity",
"description": "The granularity of each bucket."
},
"period": {
"$ref": "#/components/schemas/HistoryPeriod",
"description": "The period that was queried."
},
"workflow": {
"description": "Workflow name filter, if applied.",
"nullable": true,
"type": "string"
}
},
"required": [
"period",
"granularity",
"buckets"
],
"type": "object"
},
"StatsResponse": {
"description": "Aggregate statistics response.\n\nComputed from all runs in the store.\n\n# Examples\n\n```\nuse ironflow_api::entities::StatsResponse;\n```",
"properties": {
"active_runs": {
"description": "Number of pending or running runs.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"cancelled_runs": {
"description": "Number of cancelled runs.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"completed_runs": {
"description": "Number of completed runs.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"failed_runs": {
"description": "Number of failed runs.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"success_rate_percent": {
"description": "Success rate: completed / (completed + failed), as a percentage.",
"format": "double",
"type": "number"
},
"total_cost_usd": {
"description": "Aggregated cost across all runs in USD.",
"format": "double",
"type": "number"
},
"total_duration_ms": {
"description": "Aggregated duration across all runs in milliseconds.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"total_runs": {
"description": "Total number of runs.",
"format": "int64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"total_runs",
"completed_runs",
"failed_runs",
"cancelled_runs",
"active_runs",
"success_rate_percent",
"total_cost_usd",
"total_duration_ms"
],
"type": "object"
},
"StepCompletedEvent": {
"description": "Payload of the `Event::StepCompleted` event.\n\n# Examples\n\n```\nuse chrono::Utc;\nuse ironflow_engine::notify::StepCompletedEvent;\nuse ironflow_store::models::StepKind;\nuse rust_decimal::Decimal;\nuse uuid::Uuid;\n\nlet payload = StepCompletedEvent {\n run_id: Uuid::now_v7(),\n step_id: Uuid::now_v7(),\n step_name: \"build\".to_string(),\n kind: StepKind::Shell,\n duration_ms: 1200,\n cost_usd: Decimal::ZERO,\n at: Utc::now(),\n};\nassert_eq!(payload.step_name, \"build\");\n```",
"properties": {
"at": {
"description": "When the step completed.",
"format": "date-time",
"type": "string"
},
"cost_usd": {
"description": "Step cost in USD.",
"format": "double",
"type": "number"
},
"duration_ms": {
"description": "Step duration in milliseconds.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"kind": {
"description": "Step operation kind.",
"type": "string"
},
"run_id": {
"description": "Run identifier.",
"format": "uuid",
"type": "string"
},
"step_id": {
"description": "Step identifier.",
"format": "uuid",
"type": "string"
},
"step_name": {
"description": "Human-readable step name.",
"type": "string"
}
},
"required": [
"run_id",
"step_id",
"step_name",
"kind",
"duration_ms",
"cost_usd",
"at"
],
"type": "object"
},
"StepFailedEvent": {
"description": "Payload of the `Event::StepFailed` event.\n\n# Examples\n\n```\nuse chrono::Utc;\nuse ironflow_engine::notify::StepFailedEvent;\nuse ironflow_store::models::StepKind;\nuse uuid::Uuid;\n\nlet payload = StepFailedEvent {\n run_id: Uuid::now_v7(),\n step_id: Uuid::now_v7(),\n step_name: \"build\".to_string(),\n kind: StepKind::Shell,\n error: \"exit code 1\".to_string(),\n at: Utc::now(),\n};\nassert_eq!(payload.error, \"exit code 1\");\n```",
"properties": {
"at": {
"description": "When the step failed.",
"format": "date-time",
"type": "string"
},
"error": {
"description": "Error message.",
"type": "string"
},
"kind": {
"description": "Step operation kind.",
"type": "string"
},
"run_id": {
"description": "Run identifier.",
"format": "uuid",
"type": "string"
},
"step_id": {
"description": "Step identifier.",
"format": "uuid",
"type": "string"
},
"step_name": {
"description": "Human-readable step name.",
"type": "string"
}
},
"required": [
"run_id",
"step_id",
"step_name",
"kind",
"error",
"at"
],
"type": "object"
},
"StepResponse": {
"description": "Step response DTO — public API representation of a step.\n\n# Examples\n\n```\nuse ironflow_store::models::Step;\nuse ironflow_api::entities::StepResponse;\n```",
"properties": {
"artifacts": {
"description": "Files this step produced, downloadable through the artifact route.\n\nEmpty when the step produced none or when artifact storage is not\nconfigured on the server.",
"items": {
"$ref": "#/components/schemas/ArtifactResponse"
},
"type": "array"
},
"attempt": {
"description": "Which run attempt produced this step (1-based).\n\nA run retried twice exposes steps with `attempt` 1, 2 and 3. Steps from\nearlier attempts are kept so a failed attempt stays inspectable.",
"format": "int32",
"minimum": 0,
"type": "integer"
},
"completed_at": {
"description": "When execution completed.",
"format": "date-time",
"nullable": true,
"type": "string"
},
"cost_usd": {
"description": "Cost in USD.",
"format": "double",
"type": "number"
},
"created_at": {
"description": "When created.",
"format": "date-time",
"type": "string"
},
"debug_messages": {
"description": "Verbose conversation trace for agent steps (thinking blocks, tool\ncalls, tool results, per-turn usage). `None` when verbose mode was\noff or the step is not an agent step."
},
"dependencies": {
"description": "IDs of steps this step depends on (direct dependencies).",
"items": {
"format": "uuid",
"type": "string"
},
"type": "array"
},
"duration_ms": {
"description": "Execution duration in milliseconds.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"error": {
"description": "Optional error message.",
"nullable": true,
"type": "string"
},
"id": {
"description": "Unique step identifier.",
"format": "uuid",
"type": "string"
},
"input": {
"additionalProperties": {},
"description": "Input configuration.",
"nullable": true,
"propertyNames": {
"type": "string"
},
"type": "object"
},
"input_tokens": {
"description": "Input token count (agent steps).",
"format": "int64",
"minimum": 0,
"nullable": true,
"type": "integer"
},
"kind": {
"description": "Step operation type.",
"type": "string"
},
"name": {
"description": "Step name.",
"type": "string"
},
"output": {
"additionalProperties": {},
"description": "Step output.",
"nullable": true,
"propertyNames": {
"type": "string"
},
"type": "object"
},
"output_tokens": {
"description": "Output token count (agent steps).",
"format": "int64",
"minimum": 0,
"nullable": true,
"type": "integer"
},
"position": {
"description": "Execution order (0-based).",
"format": "int32",
"minimum": 0,
"type": "integer"
},
"run_id": {
"description": "Parent run ID.",
"format": "uuid",
"type": "string"
},
"started_at": {
"description": "When execution started.",
"format": "date-time",
"nullable": true,
"type": "string"
},
"status": {
"$ref": "#/components/schemas/StepStatus",
"description": "Current status."
},
"trace_id": {
"description": "Deterministic trace ID for log correlation.",
"format": "uuid",
"type": "string"
},
"updated_at": {
"description": "When updated.",
"format": "date-time",
"type": "string"
}
},
"required": [
"id",
"trace_id",
"run_id",
"name",
"kind",
"position",
"status",
"attempt",
"duration_ms",
"cost_usd",
"created_at",
"updated_at",
"dependencies"
],
"type": "object"
},
"StepStatus": {
"description": "Status of an individual step within a run.\n\n# Examples\n\n```\nuse ironflow_store::entities::StepStatus;\n\nassert!(!StepStatus::Pending.is_terminal());\nassert!(StepStatus::Completed.is_terminal());\n```",
"enum": [
"pending",
"running",
"completed",
"failed",
"skipped",
"awaiting_approval",
"rejected"
],
"type": "string"
},
"SubWorkflowDetail": {
"description": "Sub-workflow detail included in the workflow response.",
"properties": {
"description": {
"description": "Human-readable description.",
"type": "string"
},
"name": {
"description": "Sub-workflow name.",
"type": "string"
},
"source_code": {
"description": "Optional Rust source code of the handler.",
"nullable": true,
"type": "string"
}
},
"required": [
"name",
"description"
],
"type": "object"
},
"TriggerKind": {
"description": "How a run was triggered.\n\n# Examples\n\n```\nuse ironflow_store::entities::TriggerKind;\n\nlet trigger = TriggerKind::Manual;\nlet json = serde_json::to_string(&trigger).unwrap();\nassert!(json.contains(\"manual\"));\n```",
"oneOf": [
{
"description": "Triggered manually (CLI or programmatic call).",
"properties": {
"kind": {
"enum": [
"manual"
],
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
{
"description": "Triggered by an incoming webhook.",
"properties": {
"kind": {
"enum": [
"webhook"
],
"type": "string"
},
"path": {
"description": "The webhook path that received the request.",
"type": "string"
}
},
"required": [
"path",
"kind"
],
"type": "object"
},
{
"description": "Triggered by a cron schedule.",
"properties": {
"kind": {
"enum": [
"cron"
],
"type": "string"
},
"schedule": {
"description": "The cron expression that fired.",
"type": "string"
}
},
"required": [
"schedule",
"kind"
],
"type": "object"
},
{
"description": "Triggered via the REST API.",
"properties": {
"kind": {
"enum": [
"api"
],
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
{
"description": "Retry of a previously failed run.",
"properties": {
"kind": {
"enum": [
"retry"
],
"type": "string"
},
"parent_run_id": {
"description": "The original run that failed.",
"format": "uuid",
"type": "string"
}
},
"required": [
"parent_run_id",
"kind"
],
"type": "object"
},
{
"description": "Triggered by a parent workflow as a sub-workflow step.",
"properties": {
"kind": {
"enum": [
"workflow"
],
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
{
"description": "Triggered by a message consumed from a NATS subject.",
"properties": {
"kind": {
"enum": [
"nats"
],
"type": "string"
},
"subject": {
"description": "The NATS subject the message was consumed from.",
"type": "string"
}
},
"required": [
"subject",
"kind"
],
"type": "object"
},
{
"description": "Triggered by an internal run event (workflow chaining).",
"properties": {
"event_kind": {
"description": "The event kind that fired (e.g. `\"run_failed\"`).",
"type": "string"
},
"kind": {
"enum": [
"run_event"
],
"type": "string"
},
"source_run_id": {
"description": "The run whose event triggered this run.",
"format": "uuid",
"type": "string"
}
},
"required": [
"source_run_id",
"event_kind",
"kind"
],
"type": "object"
},
{
"description": "Triggered by a polling probe detecting new data.",
"properties": {
"kind": {
"enum": [
"polling"
],
"type": "string"
},
"probe": {
"description": "Name of the probe that fired (e.g. `\"http\"`, `\"sql\"`).",
"type": "string"
}
},
"required": [
"probe",
"kind"
],
"type": "object"
}
]
},
"UpdateRoleRequest": {
"description": "Request body for updating a user's role (admin only).",
"properties": {
"is_admin": {
"description": "New admin status.",
"type": "boolean"
}
},
"required": [
"is_admin"
],
"type": "object"
},
"UpdateScheduleRequest": {
"description": "Update schedule request body. All fields optional.",
"properties": {
"cron_expression": {
"description": "New cron expression.",
"nullable": true,
"type": "string"
},
"inputs": {
"description": "New inputs payload."
}
},
"type": "object"
},
"UpdateSecretRequest": {
"description": "Request body for updating a secret value.",
"properties": {
"value": {
"description": "New secret value (plaintext, will be encrypted at rest).",
"type": "string"
}
},
"required": [
"value"
],
"type": "object"
},
"UserResponse": {
"description": "Response DTO for a user (never exposes password hash).",
"properties": {
"created_at": {
"description": "Creation timestamp.",
"format": "date-time",
"type": "string"
},
"email": {
"description": "Email address.",
"type": "string"
},
"id": {
"description": "User ID.",
"format": "uuid",
"type": "string"
},
"is_admin": {
"description": "Whether the user is an admin.",
"type": "boolean"
},
"updated_at": {
"description": "Last update timestamp.",
"format": "date-time",
"type": "string"
},
"username": {
"description": "Display username.",
"type": "string"
}
},
"required": [
"id",
"email",
"username",
"is_admin",
"created_at",
"updated_at"
],
"type": "object"
},
"UserSignedInEvent": {
"description": "Payload of the `Event::UserSignedIn` event.\n\n# Examples\n\n```\nuse chrono::Utc;\nuse ironflow_engine::notify::UserSignedInEvent;\nuse uuid::Uuid;\n\nlet payload = UserSignedInEvent {\n user_id: Uuid::now_v7(),\n username: \"alice\".to_string(),\n at: Utc::now(),\n};\nassert_eq!(payload.username, \"alice\");\n```",
"properties": {
"at": {
"description": "When the sign-in occurred.",
"format": "date-time",
"type": "string"
},
"user_id": {
"description": "User identifier.",
"format": "uuid",
"type": "string"
},
"username": {
"description": "Username.",
"type": "string"
}
},
"required": [
"user_id",
"username",
"at"
],
"type": "object"
},
"UserSignedOutEvent": {
"description": "Payload of the `Event::UserSignedOut` event.\n\n# Examples\n\n```\nuse chrono::Utc;\nuse ironflow_engine::notify::UserSignedOutEvent;\nuse uuid::Uuid;\n\nlet user_id = Uuid::now_v7();\nlet payload = UserSignedOutEvent {\n user_id,\n at: Utc::now(),\n};\nassert_eq!(payload.user_id, user_id);\n```",
"properties": {
"at": {
"description": "When the sign-out occurred.",
"format": "date-time",
"type": "string"
},
"user_id": {
"description": "User identifier.",
"format": "uuid",
"type": "string"
}
},
"required": [
"user_id",
"at"
],
"type": "object"
},
"UserSignedUpEvent": {
"description": "Payload of the `Event::UserSignedUp` event.\n\n# Examples\n\n```\nuse chrono::Utc;\nuse ironflow_engine::notify::UserSignedUpEvent;\nuse uuid::Uuid;\n\nlet payload = UserSignedUpEvent {\n user_id: Uuid::now_v7(),\n username: \"alice\".to_string(),\n at: Utc::now(),\n};\nassert_eq!(payload.username, \"alice\");\n```",
"properties": {
"at": {
"description": "When the sign-up occurred.",
"format": "date-time",
"type": "string"
},
"user_id": {
"description": "User identifier.",
"format": "uuid",
"type": "string"
},
"username": {
"description": "Username.",
"type": "string"
}
},
"required": [
"user_id",
"username",
"at"
],
"type": "object"
},
"WorkflowAgentStepTokensUsedEvent": {
"description": "Payload of the `WorkflowEvent::AgentStepTokensUsed` workflow event.\n\n# Examples\n\n```\nuse ironflow_engine::notify::WorkflowAgentStepTokensUsedEvent;\nuse rust_decimal::Decimal;\n\nlet payload = WorkflowAgentStepTokensUsedEvent {\n step_name: \"review\".to_string(),\n tokens: 15_000,\n cost_usd: Decimal::new(42, 4),\n};\nassert_eq!(payload.tokens, 15_000);\n```",
"properties": {
"cost_usd": {
"description": "Estimated cost in USD.",
"format": "double",
"type": "number"
},
"step_name": {
"description": "Human-readable step name.",
"type": "string"
},
"tokens": {
"description": "Total tokens consumed.",
"format": "int64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"step_name",
"tokens",
"cost_usd"
],
"type": "object"
},
"WorkflowApprovalRequiredEvent": {
"description": "Payload of the `WorkflowEvent::ApprovalRequired` workflow event.\n\n# Examples\n\n```\nuse ironflow_engine::notify::WorkflowApprovalRequiredEvent;\nuse uuid::Uuid;\n\nlet payload = WorkflowApprovalRequiredEvent {\n step_name: \"prod-gate\".to_string(),\n step_index: 3,\n approval_id: Uuid::now_v7(),\n};\nassert_eq!(payload.step_name, \"prod-gate\");\n```",
"properties": {
"approval_id": {
"description": "Identifier of the approval gate.",
"format": "uuid",
"type": "string"
},
"step_index": {
"description": "Zero-based position in the workflow.",
"format": "int32",
"minimum": 0,
"type": "integer"
},
"step_name": {
"description": "Human-readable step name.",
"type": "string"
}
},
"required": [
"step_name",
"step_index",
"approval_id"
],
"type": "object"
},
"WorkflowDetailResponse": {
"description": "Workflow detail response.",
"properties": {
"category": {
"description": "Optional `/`-separated category path used to group workflows.",
"nullable": true,
"type": "string"
},
"compatible_versions": {
"description": "Versions accepted for replay without `force`.",
"items": {
"type": "string"
},
"type": "array"
},
"default_labels": {
"additionalProperties": {
"type": "string"
},
"description": "Labels automatically applied to every run of this workflow.",
"propertyNames": {
"type": "string"
},
"type": "object"
},
"default_max_cost_usd": {
"description": "Default cumulative cost cap applied to runs of this workflow, in USD.\n\nOverridden by a cap supplied at run creation. `None` means the workflow\ndeclares no default and falls back to the server default (if any).",
"format": "double",
"nullable": true,
"type": "number"
},
"description": {
"description": "Human-readable description.",
"type": "string"
},
"input_schema": {
"description": "JSON Schema describing the expected input payload."
},
"name": {
"description": "Workflow name.",
"type": "string"
},
"schedule": {
"description": "Optional 6-field cron expression for automatic execution.",
"nullable": true,
"type": "string"
},
"source_code": {
"description": "Optional Rust source code of the handler.",
"nullable": true,
"type": "string"
},
"sub_workflows": {
"description": "Sub-workflows invoked by this handler (recursive, depth-limited).",
"items": {
"$ref": "#/components/schemas/SubWorkflowDetail"
},
"type": "array"
},
"version": {
"description": "Current handler version.",
"nullable": true,
"type": "string"
}
},
"required": [
"name",
"description",
"sub_workflows"
],
"type": "object"
},
"WorkflowEvent": {
"description": "A granular step-level event for real-time workflow monitoring.\n\nUnlike [`Event`](super::Event) which covers the full system lifecycle\n(runs, auth, audit), `WorkflowEvent` tracks individual step transitions\nwithin a single run. Serialized with a `type` discriminant for UI\nconsumption.\n\nEach variant wraps a dedicated payload struct; the serialized form stays\nflat, with `type` sitting next to the payload fields.\n\n# Examples\n\n```\nuse ironflow_engine::notify::{WorkflowEvent, WorkflowStepStartedEvent};\nuse chrono::Utc;\n\nlet event = WorkflowEvent::StepStarted(WorkflowStepStartedEvent {\n step_name: \"deploy\".to_string(),\n step_index: 0,\n timestamp: Utc::now(),\n});\nassert_eq!(event.event_type(), \"step_started\");\n\nlet json = serde_json::to_string(&event)?;\nassert!(json.contains(\"\\\"type\\\":\\\"step_started\\\"\"));\n# Ok::<(), serde_json::Error>(())\n```",
"oneOf": [
{
"allOf": [
{
"$ref": "#/components/schemas/WorkflowStepStartedEvent",
"description": "A step began execution."
},
{
"properties": {
"type": {
"enum": [
"step_started"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A step began execution."
},
{
"allOf": [
{
"$ref": "#/components/schemas/WorkflowStepCompletedEvent",
"description": "A step completed successfully."
},
{
"properties": {
"type": {
"enum": [
"step_completed"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A step completed successfully."
},
{
"allOf": [
{
"$ref": "#/components/schemas/WorkflowStepFailedEvent",
"description": "A step failed."
},
{
"properties": {
"type": {
"enum": [
"step_failed"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A step failed."
},
{
"allOf": [
{
"$ref": "#/components/schemas/WorkflowApprovalRequiredEvent",
"description": "A step requires human approval before the run can continue."
},
{
"properties": {
"type": {
"enum": [
"approval_required"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "A step requires human approval before the run can continue."
},
{
"allOf": [
{
"$ref": "#/components/schemas/WorkflowAgentStepTokensUsedEvent",
"description": "Token usage report for an agent step."
},
{
"properties": {
"type": {
"enum": [
"agent_step_tokens_used"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
],
"description": "Token usage report for an agent step."
}
]
},
"WorkflowStepCompletedEvent": {
"description": "Payload of the `WorkflowEvent::StepCompleted` workflow event.\n\n# Examples\n\n```\nuse ironflow_engine::notify::WorkflowStepCompletedEvent;\n\nlet payload = WorkflowStepCompletedEvent {\n step_name: \"deploy\".to_string(),\n step_index: 1,\n duration_ms: 5000,\n output_summary: Some(\"deployed v1.2.3\".to_string()),\n};\nassert_eq!(payload.duration_ms, 5000);\n```",
"properties": {
"duration_ms": {
"description": "Step duration in milliseconds.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"output_summary": {
"description": "Optional summary of the step output.",
"nullable": true,
"type": "string"
},
"step_index": {
"description": "Zero-based position in the workflow.",
"format": "int32",
"minimum": 0,
"type": "integer"
},
"step_name": {
"description": "Human-readable step name.",
"type": "string"
}
},
"required": [
"step_name",
"step_index",
"duration_ms"
],
"type": "object"
},
"WorkflowStepFailedEvent": {
"description": "Payload of the `WorkflowEvent::StepFailed` workflow event.\n\n# Examples\n\n```\nuse ironflow_engine::notify::WorkflowStepFailedEvent;\n\nlet payload = WorkflowStepFailedEvent {\n step_name: \"test\".to_string(),\n step_index: 2,\n error: \"exit code 1\".to_string(),\n duration_ms: 3000,\n};\nassert_eq!(payload.error, \"exit code 1\");\n```",
"properties": {
"duration_ms": {
"description": "Step duration in milliseconds.",
"format": "int64",
"minimum": 0,
"type": "integer"
},
"error": {
"description": "Error description.",
"type": "string"
},
"step_index": {
"description": "Zero-based position in the workflow.",
"format": "int32",
"minimum": 0,
"type": "integer"
},
"step_name": {
"description": "Human-readable step name.",
"type": "string"
}
},
"required": [
"step_name",
"step_index",
"error",
"duration_ms"
],
"type": "object"
},
"WorkflowStepStartedEvent": {
"description": "Payload of the `WorkflowEvent::StepStarted` workflow event.\n\n# Examples\n\n```\nuse chrono::Utc;\nuse ironflow_engine::notify::WorkflowStepStartedEvent;\n\nlet payload = WorkflowStepStartedEvent {\n step_name: \"build\".to_string(),\n step_index: 0,\n timestamp: Utc::now(),\n};\nassert_eq!(payload.step_index, 0);\n```",
"properties": {
"step_index": {
"description": "Zero-based position in the workflow.",
"format": "int32",
"minimum": 0,
"type": "integer"
},
"step_name": {
"description": "Human-readable step name.",
"type": "string"
},
"timestamp": {
"description": "When the step started.",
"format": "date-time",
"type": "string"
}
},
"required": [
"step_name",
"step_index",
"timestamp"
],
"type": "object"
},
"WorkflowSummary": {
"description": "Summary entry returned by `GET /api/v1/workflows`.",
"properties": {
"category": {
"description": "Optional `/`-separated category path.",
"nullable": true,
"type": "string"
},
"name": {
"description": "Workflow name (unique identifier).",
"type": "string"
},
"schedule": {
"description": "Optional 6-field cron expression for automatic execution.",
"nullable": true,
"type": "string"
},
"version": {
"description": "Current handler version.",
"nullable": true,
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
}
},
"securitySchemes": {
"Bearer": {
"bearerFormat": "JWT",
"scheme": "bearer",
"type": "http"
}
}
},
"info": {
"contact": {
"name": "Thomas Tartrau"
},
"description": "REST API for the ironflow workflow engine",
"license": {
"identifier": "MIT",
"name": "MIT"
},
"title": "Ironflow REST API",
"version": "1.0.0"
},
"openapi": "3.0.3",
"paths": {
"/api/v1/api-keys": {
"get": {
"operationId": "list_api_keys",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/ApiKeyResponse"
},
"type": "array"
}
}
},
"description": "List of API keys"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "List all API keys for the authenticated user.",
"tags": [
"api-keys"
]
},
"post": {
"description": "# Errors\n\n- 400 if the name is empty or scopes are invalid",
"operationId": "create_api_key",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateApiKeyRequest"
}
}
},
"description": "API key configuration",
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateApiKeyResponse"
}
}
},
"description": "API key created successfully"
},
"400": {
"description": "Invalid input"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden (member trying to assign forbidden scopes)"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Create a new API key for the authenticated user.",
"tags": [
"api-keys"
]
}
},
"/api/v1/api-keys/scopes": {
"get": {
"description": "Admins get all scopes, members only get read-only scopes.",
"operationId": "available_scopes",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/ScopeEntry"
},
"type": "array"
}
}
},
"description": "List of available scopes"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Return the list of scopes the current user is allowed to assign to API keys.",
"tags": [
"api-keys"
]
}
},
"/api/v1/api-keys/{id}": {
"delete": {
"description": "# Errors\n\n- 404 if the key does not exist or belongs to another user",
"operationId": "delete_api_key",
"parameters": [
{
"description": "API key ID",
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"204": {
"description": "API key deleted successfully"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "API key not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Delete an API key owned by the authenticated user.",
"tags": [
"api-keys"
]
}
},
"/api/v1/audit-logs": {
"get": {
"description": "Admin-only. Returns a paginated list of persisted domain events\nfor compliance review and post-mortem debugging.",
"operationId": "list_audit_logs",
"parameters": [
{
"description": "Filter by event type (e.g. `run_status_changed`).",
"in": "query",
"name": "event_type",
"required": false,
"schema": {
"$ref": "#/components/schemas/EventKind"
}
},
{
"description": "Filter by run ID.",
"in": "query",
"name": "run_id",
"required": false,
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"description": "Filter entries created at or after this timestamp.",
"in": "query",
"name": "from",
"required": false,
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"description": "Filter entries created at or before this timestamp.",
"in": "query",
"name": "to",
"required": false,
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"description": "Page number (1-based, default: 1).",
"in": "query",
"name": "page",
"required": false,
"schema": {
"format": "int32",
"minimum": 0,
"type": "integer"
}
},
{
"description": "Items per page (default: 50, max: 100).",
"in": "query",
"name": "per_page",
"required": false,
"schema": {
"format": "int32",
"minimum": 0,
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/AuditLogEntry"
},
"type": "array"
}
}
},
"description": "List of audit log entries with pagination"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden - admin only"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "List audit log entries with optional filtering and pagination.",
"tags": [
"audit"
]
}
},
"/api/v1/auth/me": {
"get": {
"description": "# Errors\n\n- 401 if no valid token is provided\n- 404 if the user no longer exists in the store",
"operationId": "me",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MeResponse"
}
}
},
"description": "Current user profile"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Return the current user's profile.",
"tags": [
"auth"
]
}
},
"/api/v1/auth/password": {
"patch": {
"description": "Verifies the old password, hashes the new one, and persists it.\n\n# Errors\n\n- 400 if old password is incorrect or new password fails validation\n- 401 if no valid token is provided\n- 404 if the user no longer exists in the store",
"operationId": "change_password",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChangePasswordRequest"
}
}
},
"description": "Old and new passwords",
"required": true
},
"responses": {
"200": {
"description": "Password changed successfully"
},
"400": {
"description": "Invalid old password or validation error"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Change the authenticated user's password.",
"tags": [
"auth"
]
}
},
"/api/v1/auth/refresh": {
"post": {
"description": "# Errors\n\n- 401 if the refresh token is missing, invalid, or expired",
"operationId": "refresh",
"responses": {
"204": {
"description": "Access token refreshed successfully, new cookies set"
},
"401": {
"description": "Invalid or expired refresh token"
}
},
"summary": "Refresh the access token using a valid refresh token from cookies.",
"tags": [
"auth"
]
}
},
"/api/v1/auth/sign-in": {
"post": {
"description": "Returns access and refresh tokens on success, and sets HttpOnly cookies.\n\n# Errors\n\n- 401 if email not found or password is wrong",
"operationId": "sign_in",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SignInRequest"
}
}
},
"description": "Sign in credentials",
"required": true
},
"responses": {
"204": {
"description": "User authenticated successfully, cookies set"
},
"401": {
"description": "Invalid email or password"
}
},
"summary": "Authenticate a user with email and password.",
"tags": [
"auth"
]
}
},
"/api/v1/auth/sign-out": {
"post": {
"operationId": "sign_out",
"responses": {
"200": {
"description": "User signed out successfully, cookies cleared"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Sign out the current user by clearing auth cookies.",
"tags": [
"auth"
]
}
},
"/api/v1/auth/sign-up": {
"post": {
"description": "Returns access and refresh tokens on success, and sets HttpOnly cookies.\n\n# Errors\n\n- 400 if email/username/password is invalid\n- 409 if email or username is already taken",
"operationId": "sign_up",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SignUpRequest"
}
}
},
"description": "Sign up credentials",
"required": true
},
"responses": {
"204": {
"description": "User registered successfully, cookies set"
},
"400": {
"description": "Invalid email, username, or password"
},
"409": {
"description": "Email or username already taken"
}
},
"summary": "Register a new user with email and password.",
"tags": [
"auth"
]
}
},
"/api/v1/health-check": {
"get": {
"operationId": "health_check",
"responses": {
"200": {
"description": "Service is healthy"
}
},
"summary": "Health check handler. Always returns 200 OK.",
"tags": [
"health"
]
}
},
"/api/v1/runs": {
"get": {
"description": "# Query Parameters\n\n- `workflow` — Filter by workflow name (optional)\n- `status` — Filter by run status (optional)\n- `created_by` — Filter by author user ID (optional). Also matches runs\n triggered by one of that user's API keys.\n- `page` — Page number, 1-based (default: 1)\n- `per_page` — Items per page (default: 20, max: 100)",
"operationId": "list_runs",
"parameters": [
{
"description": "Filter by workflow name.",
"in": "query",
"name": "workflow",
"required": false,
"schema": {
"type": "string"
}
},
{
"description": "Filter by run status.",
"in": "query",
"name": "status",
"required": false,
"schema": {
"$ref": "#/components/schemas/RunStatus"
}
},
{
"description": "Filter by step presence (only applies to completed/cancelled runs).\nNon-terminal runs (pending, running, etc.) are always included.\nWhen `true`, only return completed/cancelled runs that have steps.\nWhen `false`, only return completed/cancelled runs without steps.",
"in": "query",
"name": "has_steps",
"required": false,
"schema": {
"type": "boolean"
}
},
{
"description": "Filter by labels. Comma-separated `key:value` pairs.",
"in": "query",
"name": "label",
"required": false,
"schema": {
"type": "string"
}
},
{
"description": "Filter by author: the user ID that triggered the run.\n\nAlso matches runs triggered by one of that user's API keys.",
"in": "query",
"name": "created_by",
"required": false,
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"description": "Page number (1-based).",
"in": "query",
"name": "page",
"required": false,
"schema": {
"format": "int32",
"minimum": 0,
"type": "integer"
}
},
{
"description": "Items per page.",
"in": "query",
"name": "per_page",
"required": false,
"schema": {
"format": "int32",
"minimum": 0,
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/RunResponse"
},
"type": "array"
}
}
},
"description": "List of runs with pagination"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "List runs with optional filtering and pagination.",
"tags": [
"runs"
]
},
"post": {
"description": "Returns 201 Created with the newly enqueued run.\n\nAn optional `Idempotency-Key` header makes the call safe to replay: the same\nkey returns the run it already produced with 200 OK instead of enqueueing a\nsecond one. A key reused with a different workflow or payload is rejected with\n409 Conflict. Keys stay bound for 24 hours, after which they are released.\n\n# Errors\n\nReturns [`ApiError::Forbidden`] for non-admin callers.\nReturns [`ApiError::BadRequest`] if the workflow is unknown, the body is\ninvalid, or the `Idempotency-Key` header is malformed.\nReturns [`ApiError::IdempotencyKeyConflict`] if the key is bound to a\ndifferent request.\nReturns [`ApiError::MonthlyBudgetExceeded`] if the global monthly cost quota\nis exhausted.",
"operationId": "create_run",
"parameters": [
{
"description": "Optional key making the call safe to replay. At most 255 printable ASCII characters, valid for 24 hours.",
"in": "header",
"name": "Idempotency-Key",
"required": false,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateRunRequest"
}
}
},
"description": "Workflow to trigger",
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RunResponse"
}
}
},
"description": "Idempotency key replayed: the existing run is returned"
},
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RunResponse"
}
}
},
"description": "Run created successfully"
},
"400": {
"description": "Unknown workflow, invalid body or malformed Idempotency-Key"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"409": {
"description": "Idempotency key already used with a different request"
},
"429": {
"description": "Monthly cost quota exhausted"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Trigger a workflow by name.",
"tags": [
"runs"
]
}
},
"/api/v1/runs/{id}": {
"get": {
"description": "Returns 404 if the run does not exist.",
"operationId": "get_run",
"parameters": [
{
"description": "Run ID",
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RunDetailResponse"
}
}
},
"description": "Run details with steps"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Run not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Get a run by ID, including all its steps and dependency edges.",
"tags": [
"runs"
]
}
},
"/api/v1/runs/{id}/approve": {
"post": {
"description": "Transitions the run from `AwaitingApproval` back to `Running`.\nReturns 400 if the run is not in `AwaitingApproval` state.",
"operationId": "approve_run",
"parameters": [
{
"description": "Run ID",
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RunResponse"
}
}
},
"description": "Run approved successfully"
},
"400": {
"description": "Run not awaiting approval"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Run not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Approve a run that is awaiting human approval.",
"tags": [
"runs"
]
}
},
"/api/v1/runs/{id}/cancel": {
"post": {
"description": "Transitions the run to `Cancelled` status. Returns 400 if the run\nis already in a terminal state.",
"operationId": "cancel_run",
"parameters": [
{
"description": "Run ID",
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RunResponse"
}
}
},
"description": "Run cancelled successfully"
},
"400": {
"description": "Run cannot be cancelled"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Run not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Cancel a pending or running run.",
"tags": [
"runs"
]
}
},
"/api/v1/runs/{id}/events": {
"get": {
"description": "Streams [`WorkflowEvent`]s for a specific workflow run in real time.\nSupports optional filtering via `?types=step_started,step_completed`.\n\nEach SSE message has:\n- `event:` set to the event type (e.g. `step_started`)\n- `data:` JSON-serialized event payload\n\nA keep-alive comment is sent every 30 seconds.\n\n# Errors\n\nReturns 401 if the request is not authenticated.\nReturns 404 if the run does not exist.",
"operationId": "run_events",
"parameters": [
{
"description": "Run ID",
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"description": "Comma-separated workflow event types to filter (e.g. step_started,step_completed)",
"in": "query",
"name": "types",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "SSE stream of workflow events"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Run not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "`GET /api/v1/runs/{id}/events` -- per-run Server-Sent Events stream.",
"tags": [
"runs"
]
}
},
"/api/v1/runs/{id}/logs": {
"get": {
"description": "Returns log entries ordered by time (UUID v7 ascending). Use the\n`cursor` query parameter with the last entry's `id` to fetch the\nnext page.",
"operationId": "get_run_logs",
"parameters": [
{
"description": "Run ID",
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"description": "Filter by step ID.",
"in": "query",
"name": "step_id",
"required": false,
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"description": "Filter by output stream (`stdout`, `stderr`, `system`).",
"in": "query",
"name": "stream",
"required": false,
"schema": {
"$ref": "#/components/schemas/LogStream"
}
},
{
"description": "Cursor for pagination (last entry ID from previous page).",
"in": "query",
"name": "cursor",
"required": false,
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"description": "Number of entries to return (default: 100, max: 1000).",
"in": "query",
"name": "limit",
"required": false,
"schema": {
"format": "int32",
"minimum": 0,
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/LogEntry"
},
"type": "array"
}
}
},
"description": "Log entries with cursor-based pagination"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Run not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Retrieve persisted log lines for a run with cursor-based pagination.",
"tags": [
"runs"
]
}
},
"/api/v1/runs/{id}/reject": {
"post": {
"description": "Transitions the run from `AwaitingApproval` to `Failed`.\nReturns 400 if the run is not in `AwaitingApproval` state.",
"operationId": "reject_run",
"parameters": [
{
"description": "Run ID",
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RunResponse"
}
}
},
"description": "Run rejected successfully"
},
"400": {
"description": "Run not awaiting approval"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Run not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Reject a run that is awaiting human approval.",
"tags": [
"runs"
]
}
},
"/api/v1/runs/{id}/retry": {
"post": {
"description": "Creates a new `Pending` run with `TriggerKind::Retry` pointing to the\noriginal. Returns 400 if the run is not in a retryable state, 409 if an\nautomatic retry is already armed, and 409 if the handler version has\nchanged since the original run (pass `?force=true` to override).",
"operationId": "retry_run",
"parameters": [
{
"description": "Run ID",
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"description": "Force retry despite handler version mismatch",
"in": "query",
"name": "force",
"required": false,
"schema": {
"type": "boolean"
}
}
],
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RunResponse"
}
}
},
"description": "Run retry created successfully"
},
"400": {
"description": "Run cannot be retried"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Run not found"
},
"409": {
"description": "Version mismatch or automatic retry already armed"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Retry a failed run.",
"tags": [
"runs"
]
}
},
"/api/v1/runs/{id}/steps/{step_id}/artifacts/{name}": {
"get": {
"description": "Streams the stored bytes with the MIME type recorded at upload time. The\nartifact must belong to both the run and the step named in the path, so an\nid guessed from another run resolves to a 404 rather than a leak.\n\nRequires the same permission as reading the run itself.",
"operationId": "download_artifact",
"parameters": [
{
"description": "Run ID",
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"description": "Step ID",
"in": "path",
"name": "step_id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"description": "Artifact name",
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/octet-stream": {}
},
"description": "Artifact content"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Run, step or artifact not found"
},
"501": {
"description": "Artifact storage is not configured"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Download one artifact produced by a step.",
"tags": [
"runs"
]
}
},
"/api/v1/schedules": {
"get": {
"description": "# Errors\n\n- 401 if not authenticated",
"operationId": "list_schedules",
"parameters": [
{
"description": "Page number (1-based)",
"in": "query",
"name": "page",
"required": false,
"schema": {
"format": "int32",
"minimum": 0,
"type": "integer"
}
},
{
"description": "Items per page",
"in": "query",
"name": "per_page",
"required": false,
"schema": {
"format": "int32",
"minimum": 0,
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/ScheduleResponse"
},
"type": "array"
}
}
},
"description": "Paginated schedules"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "List all schedules, paginated.",
"tags": [
"schedules"
]
},
"post": {
"description": "# Errors\n\n- 400 if validation fails or cron expression is invalid\n- 400 if the workflow is not registered\n- 401 if not authenticated",
"operationId": "create_schedule",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateScheduleRequest"
}
}
},
"description": "Schedule definition",
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScheduleResponse"
}
}
},
"description": "Schedule created"
},
"400": {
"description": "Invalid input"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Create a new schedule.",
"tags": [
"schedules"
]
}
},
"/api/v1/schedules/{id}": {
"delete": {
"description": "Handler-declared schedules (`source = handler`) cannot be deleted via the\nAPI -- they are managed by the code and reconciled at startup.\n\n# Errors\n\n- 401 if not authenticated\n- 403 if the schedule is handler-declared\n- 404 if the schedule does not exist",
"operationId": "delete_schedule",
"parameters": [
{
"description": "Schedule ID",
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"204": {
"description": "Schedule deleted"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Cannot delete handler-declared schedule"
},
"404": {
"description": "Schedule not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Delete a schedule by ID.",
"tags": [
"schedules"
]
},
"get": {
"description": "# Errors\n\n- 401 if not authenticated\n- 404 if the schedule does not exist",
"operationId": "get_schedule",
"parameters": [
{
"description": "Schedule ID",
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScheduleResponse"
}
}
},
"description": "Schedule detail"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Schedule not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Get a schedule by ID.",
"tags": [
"schedules"
]
}
},
"/api/v1/schedules/{id}/pause": {
"post": {
"description": "# Errors\n\n- 401 if not authenticated\n- 404 if the schedule does not exist",
"operationId": "pause_schedule",
"parameters": [
{
"description": "Schedule ID",
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScheduleResponse"
}
}
},
"description": "Schedule paused"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Schedule not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Pause a schedule (set enabled = false).",
"tags": [
"schedules"
]
}
},
"/api/v1/schedules/{id}/resume": {
"post": {
"description": "# Errors\n\n- 401 if not authenticated\n- 404 if the schedule does not exist",
"operationId": "resume_schedule",
"parameters": [
{
"description": "Schedule ID",
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScheduleResponse"
}
}
},
"description": "Schedule resumed"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Schedule not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Resume a schedule (clear disabled_at).",
"tags": [
"schedules"
]
}
},
"/api/v1/schedules/{id}/trigger": {
"post": {
"description": "# Errors\n\n- 401 if not authenticated\n- 404 if the schedule does not exist",
"operationId": "trigger_schedule",
"parameters": [
{
"description": "Schedule ID",
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScheduleResponse"
}
}
},
"description": "Run created from schedule"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Schedule not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Trigger a schedule manually, creating a run immediately.",
"tags": [
"schedules"
]
}
},
"/api/v1/secrets": {
"get": {
"description": "Returns key, id, and timestamps. Never returns encrypted or decrypted values.",
"operationId": "list_secrets",
"parameters": [
{
"description": "Filter by key prefix (e.g. `workflows/inbox/`).",
"in": "query",
"name": "prefix",
"required": false,
"schema": {
"type": "string"
}
},
{
"description": "Page number (1-based, default 1).",
"in": "query",
"name": "page",
"required": false,
"schema": {
"format": "int32",
"minimum": 0,
"type": "integer"
}
},
{
"description": "Items per page (default 50, max 100).",
"in": "query",
"name": "per_page",
"required": false,
"schema": {
"format": "int32",
"minimum": 0,
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/SecretResponse"
},
"type": "array"
}
}
},
"description": "Secrets listed"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "List secret metadata. Admin only.",
"tags": [
"secrets"
]
},
"post": {
"description": "If the key already exists, the value is replaced. The response\nnever includes the secret value.",
"operationId": "create_secret",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SetSecretRequest"
}
}
},
"description": "Secret key and value",
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SecretResponse"
}
}
},
"description": "Secret created or updated"
},
"400": {
"description": "Invalid input"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Create or update a secret. Admin only.",
"tags": [
"secrets"
]
}
},
"/api/v1/secrets/key-versions": {
"get": {
"description": "This is what tells an operator whether an old key can be dropped from\n`IRONFLOW_SECRET_KEYS` without breaking the next startup: a version listed\nin `retirable` is configured, not active, and used by no secret.\n\nA non-empty `missing` means some stored secrets cannot be decrypted with\nthe current configuration -- the server refuses to start in that state.\n\n# Errors\n\n- 401 if the caller is not authenticated\n- 403 if the caller is not an admin",
"operationId": "secret_key_versions",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/KeyVersionsResponse"
}
}
},
"description": "Key ring status"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Report how the configured key ring lines up with the stored secrets.",
"tags": [
"secrets"
]
}
},
"/api/v1/secrets/rotate": {
"post": {
"description": "One call processes one batch, so a rotation over a large stock never\nbecomes a single long request. The client loops, passing `last_id` back as\n`after_id`, until `remaining` reaches zero or `last_id` comes back null.\n\nThe operation is idempotent: secrets already on the target version are\nskipped, so an interrupted rotation can simply be restarted.\n\n# Errors\n\n- 400 if the target version is not in the configured key ring\n- 401 if the caller is not authenticated\n- 403 if the caller is not an admin",
"operationId": "rotate_secrets",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RotateSecretsRequest"
}
}
},
"description": "Target key version, batch size, and resume cursor",
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RotateSecretsResponse"
}
}
},
"description": "Batch rotated"
},
"400": {
"description": "Invalid target key version"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Re-encrypt one batch of secrets towards a key version. Admin only.",
"tags": [
"secrets"
]
}
},
"/api/v1/secrets/{key}": {
"delete": {
"description": "# Errors\n\n- 403 if the caller is not an admin\n- 404 if the secret key does not exist",
"operationId": "delete_secret",
"parameters": [
{
"description": "Secret key",
"in": "path",
"name": "key",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"204": {
"description": "Secret deleted"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Secret not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Delete a secret by key. Admin only.",
"tags": [
"secrets"
]
},
"put": {
"description": "The key is taken from the URL path. The response never includes\nthe secret value.\n\n# Errors\n\n- 403 if the caller is not an admin\n- 404 if the secret key does not exist\n- 400 if the value is invalid",
"operationId": "update_secret",
"parameters": [
{
"description": "Secret key",
"in": "path",
"name": "key",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateSecretRequest"
}
}
},
"description": "New secret value",
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SecretResponse"
}
}
},
"description": "Secret updated"
},
"400": {
"description": "Invalid input"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Secret not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Update a secret's value. Admin only.",
"tags": [
"secrets"
]
}
},
"/api/v1/stats": {
"get": {
"description": "Accepts the same filtering query parameters as `GET /api/v1/runs`\n(`workflow`, `status`, `has_steps`, `label`, `created_by`). `page` and\n`per_page` are ignored.",
"operationId": "get_stats",
"parameters": [
{
"description": "Filter by workflow name.",
"in": "query",
"name": "workflow",
"required": false,
"schema": {
"type": "string"
}
},
{
"description": "Filter by run status.",
"in": "query",
"name": "status",
"required": false,
"schema": {
"$ref": "#/components/schemas/RunStatus"
}
},
{
"description": "Filter by step presence (only applies to completed/cancelled runs).\nNon-terminal runs (pending, running, etc.) are always included.\nWhen `true`, only return completed/cancelled runs that have steps.\nWhen `false`, only return completed/cancelled runs without steps.",
"in": "query",
"name": "has_steps",
"required": false,
"schema": {
"type": "boolean"
}
},
{
"description": "Filter by labels. Comma-separated `key:value` pairs.",
"in": "query",
"name": "label",
"required": false,
"schema": {
"type": "string"
}
},
{
"description": "Filter by author: the user ID that triggered the run.\n\nAlso matches runs triggered by one of that user's API keys.",
"in": "query",
"name": "created_by",
"required": false,
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"description": "Page number (1-based).",
"in": "query",
"name": "page",
"required": false,
"schema": {
"format": "int32",
"minimum": 0,
"type": "integer"
}
},
{
"description": "Items per page.",
"in": "query",
"name": "per_page",
"required": false,
"schema": {
"format": "int32",
"minimum": 0,
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StatsResponse"
}
}
},
"description": "Aggregate statistics"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Get aggregate statistics across runs matching the filter.",
"tags": [
"stats"
]
}
},
"/api/v1/stats/history": {
"get": {
"description": "Returns aggregated run counts, duration metrics, and cost per time\nbucket. Accepts optional `workflow`, `period`, and `granularity`\nquery parameters.",
"operationId": "get_stats_history",
"parameters": [
{
"description": "Filter by workflow name. Omit to aggregate all workflows.",
"in": "query",
"name": "workflow",
"required": false,
"schema": {
"type": "string"
}
},
{
"description": "Time period to query. Defaults to `7d`.",
"in": "query",
"name": "period",
"required": false,
"schema": {
"$ref": "#/components/schemas/HistoryPeriod"
}
},
{
"description": "Bucket granularity. Auto-derived from period when omitted.",
"in": "query",
"name": "granularity",
"required": false,
"schema": {
"$ref": "#/components/schemas/HistoryGranularity"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StatsHistoryResponse"
}
}
},
"description": "Historical statistics"
},
"400": {
"description": "Invalid period or granularity"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Get time-bucketed historical statistics for trend charts.",
"tags": [
"stats"
]
}
},
"/api/v1/users": {
"get": {
"description": "# Errors\n\n- 403 if the caller is not an admin",
"operationId": "list_users",
"parameters": [
{
"description": "Page number (1-based, defaults to 1).",
"in": "query",
"name": "page",
"required": false,
"schema": {
"format": "int32",
"minimum": 0,
"type": "integer"
}
},
{
"description": "Items per page (defaults to 20, max 100).",
"in": "query",
"name": "per_page",
"required": false,
"schema": {
"format": "int32",
"minimum": 0,
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/UserResponse"
},
"type": "array"
}
}
},
"description": "Paginated list of users"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden (not an admin)"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "List all users with pagination. Admin only.",
"tags": [
"users"
]
},
"post": {
"description": "# Errors\n\n- 403 if the caller is not an admin\n- 400 if input validation fails\n- 409 if email or username is already taken",
"operationId": "create_user",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateUserRequest"
}
}
},
"description": "User account details",
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserResponse"
}
}
},
"description": "User created successfully"
},
"400": {
"description": "Invalid input"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden (not an admin)"
},
"409": {
"description": "Email or username already taken"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Create a new user account. Admin only.",
"tags": [
"users"
]
}
},
"/api/v1/users/{id}": {
"delete": {
"description": "# Errors\n\n- 403 if the caller is not an admin\n- 400 if trying to delete self\n- 404 if the user does not exist",
"operationId": "delete_user",
"parameters": [
{
"description": "User ID",
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"204": {
"description": "User deleted successfully"
},
"400": {
"description": "Cannot delete self"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden (not an admin)"
},
"404": {
"description": "User not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Delete a user account. Admin only.",
"tags": [
"users"
]
}
},
"/api/v1/users/{id}/role": {
"patch": {
"description": "# Errors\n\n- 403 if the caller is not an admin\n- 400 if trying to change own role\n- 404 if the user does not exist",
"operationId": "update_role",
"parameters": [
{
"description": "User ID",
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateRoleRequest"
}
}
},
"description": "New role assignment",
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserResponse"
}
}
},
"description": "User role updated successfully"
},
"400": {
"description": "Cannot change own role"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden (not an admin)"
},
"404": {
"description": "User not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Update a user's admin role. Admin only.",
"tags": [
"users"
]
}
},
"/api/v1/workflows": {
"get": {
"description": "# Query Parameters\n\n- `name` — Case-insensitive partial match on workflow name (optional)\n- `category` — Case-insensitive partial match on category path, or\n [`UNCATEGORIZED_FILTER`] to filter only uncategorized workflows\n (optional)",
"operationId": "list_workflows",
"parameters": [
{
"description": "Optional case-insensitive partial match on workflow name.",
"in": "query",
"name": "name",
"required": false,
"schema": {
"type": "string"
}
},
{
"description": "Optional case-insensitive partial match on the category path\n(e.g. `etl` matches `Data/ETL` and `data/etl/nightly`).\n\nPass `__uncategorized__` to list only workflows without any\ncategory.",
"in": "query",
"name": "category",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/WorkflowSummary"
},
"type": "array"
}
}
},
"description": "List of workflow summaries"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "List registered workflows, optionally filtered by name and category.",
"tags": [
"workflows"
]
}
},
"/api/v1/workflows/{name}": {
"get": {
"description": "# Errors\n\n- 404 if the workflow is not registered",
"operationId": "get_workflow",
"parameters": [
{
"description": "Workflow name",
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WorkflowDetailResponse"
}
}
},
"description": "Workflow details"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Workflow not found"
}
},
"security": [
{
"Bearer": []
}
],
"summary": "Get details about a registered workflow.",
"tags": [
"workflows"
]
}
}
},
"tags": [
{
"description": "Health check endpoints",
"name": "health"
},
{
"description": "Workflow run management",
"name": "runs"
},
{
"description": "Workflow definitions",
"name": "workflows"
},
{
"description": "Aggregated statistics",
"name": "stats"
},
{
"description": "Authentication and authorization",
"name": "auth"
},
{
"description": "API key management",
"name": "api-keys"
},
{
"description": "User management (admin only)",
"name": "users"
},
{
"description": "Encrypted secret management (admin only)",
"name": "secrets"
},
{
"description": "Audit log (admin only)",
"name": "audit"
},
{
"description": "Run/step log persistence and retrieval",
"name": "logs"
},
{
"description": "Schedule management",
"name": "schedules"
}
]
}