Aevatar workflow authoring
Skill ChronoAIProject/NyxID/skills/aevatar-workflow-authoring
Author, preview, validate, and persist an executable aevatar workflow from a natural-language request. Use it when the user wants to create, build, set up, automate, or feature-probe a multi-step task as a runnable Aevatar workflow. It covers exact NyxID operation and authored-request admission, bounded YAML, file inputs, terminal run verification, feature-equivalent evidence boundaries, and reusable publication. Not for blindly rerunning an existing failed workflow.From its SKILL.md
npx -y skills add ChronoAIProject/NyxID --skill aevatar-workflow-authoringAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
SKILL.md
37.7 KB, as published. Nobody here has run it
Authoring an executable aevatar workflow
You turn a user's natural-language request into a valid, test-run, reusable aevatar workflow. A workflow is a YAML document of roles + steps that the engine executes; once validated you persist it as a skill so the user can re-run it and watch it in the observatory.
The core DSL, engine rules, and tool protocol are here. Load the linked REST or worked-example reference only when that surface is relevant. Follow the protocol in order.
Two execution surfaces — know which one you are before step 3. Steps 3 / 5 / 6 below call the server-side agent tools
nyxid_services,aevatar_start_workflow, andornn_publish_skill. Those exist only when you are the model running inside an aevatar session with the nyxid MCP connected. If instead you are an external client holding only a NyxID bearer token — driving the aevatar backend through the NyxID broker (nyxid proxy request aevatar), the same identity the sibling skills (aevatar-team-builder,aevatar-service-publisher,aevatar-scheduler) assume — those three tools are not callable. Read references/client-rest.md for the client dry-run, publication, and invocation path; the core DSL and engine rules below apply to both surfaces.
Protocol (follow in order)
- Confirm the intent is authoring. The user wants a new runnable workflow. If they want to run something that already exists, stop and search for it instead.
- Clarify just enough. Pin down: the trigger/input, how that ingress authenticates its source, the ordered steps, the desired output, and which external services (if any) are involved. Ask only what you cannot reasonably infer; do not over-interrogate.
- Select the exact external capability (only if external calls are needed). Prefer a published operation: call
list_external_workflow_capabilities, copy its exactuser_service_id + endpoint_idselector, then inspect readiness forinteractiveordurable. The YAML/proto selector has nooperation_idfield. Use typedcapability.nyxid_requestonly when no published operation represents a required HTTP request; it needs an exact UserService selected from authoritative/api/v1/keys, a typed method/path/body contract, and bind-time authenticated confirmation/grant. One step carries exactly one ofnyxid_operationornyxid_request, never both./api/v1/user-servicesis only a routing projection and is never execution authority. - Author the YAML. Apply the DSL below and obey every rule in Engine rules (must obey). Prefer the reliable-core primitives; use advanced primitives only when the task truly needs them.
- Preview before execution. Use the explicit-request preview or draft-run readiness surface. Confirm unique call sites, read/write classification, approval requirements, exact workflow/revision identity, and every blocker. Preview/readiness proves admission readiness only; it does not prove runtime credential propagation or downstream authorization.
- Run once when authorized, then observe the same run. Never use another mutation as a status check. For a real acceptance run, require terminal completion,
lastSuccess=true, non-empty required step outputs, and non-empty final output. On failure, read run detail and audit, find the first failed step, and diagnose before any rerun. Do not blindly retry a mutation or a failed workflow. If the original definition is blocked by a stale or inaccessible external resource, a separately authorized feature-equivalent probe may substitute an accessible sanitized resource while preserving the relevant step, selector, transform, file, and fan-out shapes. Its success proves only those platform features; the original workflow remains blocked or unproven. Never add sends, approvals, external mutations, or schedules merely to broaden a probe. - Persist as a reusable workflow. After structural validation and any required execution proof, call
ornn_publish_skillwith the final workflow inworkflow_yamls(see Persisting). This creates a private skill in the user's account containing the workflow. - Report evidence honestly. Separate preview-ready, accepted, completed, business-verified, blocked by policy, and failed. Do not call
acceptedor an opened SSE stream a successful run. - Iterate on request. To change an existing workflow: load it with
use_skill, edit the YAML, repeat preview and validation, and publish a new version.
Keep tool rounds bounded, but do not trade away terminal evidence. Start one authorized run, record its identity privately, and observe that same run through the run/read-model surfaces.
Engine rules (must obey)
These are the failure modes that break generated workflows. Check every one before validating.
- Single terminal step. A run ends at the step that has no
nextand is last in document order. Make the final step the last line of the document. - Bounded YAML. Each YAML document is limited to 1 MiB UTF-8, 10,000 parsed nodes, and collection nesting depth 64. Aliases do not bypass these limits. Treat a resource-limit rejection as a document defect; reduce the document instead of retrying it.
- Fall-through is by document order, not id order. A step with no
nextfalls through to the next step written in the file. So every branch must reach the terminal step via an explicitnext, and nothing should sit after the terminal step. Getting this wrong silently overwrites your output. - No ambient workflow clock. The engine does not invent "today" for a normal invocation. The
caller must inject dates through run input. A Team automation can render
{{@schedule.run_date}},run_year,run_month, anddays_until_month_endfrom its authoritative logical fire plus configured timezone; a scope webhook binding can render{{@run_date}}from received time plus its explicittimeZoneId. These are ingress contracts, not workflow expression variables. Never read host wall-clock time or ask an LLM to guess it. - Input authority follows the ingress contract. Classify the source before adding a compensating provider read. A shared or unverified endpoint authenticates neither the sender nor its claims: accept only a locator such as
record_id, then read authoritative fields through an admitted provider capability. A dedicated automation credential or HMAC-verified Host/Adapter path may treat pushed fields as workflow input only when that principal is exclusive to the source and the exact body is authenticated. Caller authentication alone is insufficient when people or unrelated systems share the credential. Preserve source/delivery provenance, make idempotency and deduplication explicit, and read the provider only for fields the trusted payload omitted, freshness after the trigger, or an independent business rule. For external-trigger wiring, hand off toaevatar-service-publisher; do not invent a second workflow lifecycle. - Role is not model.
target_roleselects the actor, not a model — never put a model name intarget_role. A role may carryprovider/model, but set them only when the user explicitly wants a specific model; otherwise omit and let the session default apply. parametersvalues are strings. Bare words are read as strings (op: trim); quote anything numeric or boolean so it stays a string (n: "50",max_iterations: "5").- Determinism for money/counts/dedup. Use
transform(sum,group_by,round, …) for any arithmetic, totals, or deduplication. Never let anllm_callcompute amounts or counts. - Transform operations are an allowlist. Supported operations are
identity,count,count_words,take,take_last,join,split,json_extract,json_parse,rss_extract_items,distinct,uppercase,lowercase,trim,reverse_lines,sum,subtract,multiply,divide,round,min,max,group_by, andtemplate. There is noreplaceorregex_replace. The current runtime treats an unknown operation as identity and can report the step successful, so reject any unlistedopduring authoring and assert the transformed output. If deterministic string replacement is essential, use a boundedcode_executestep only when its admission policy permits it; never substitute an LLM. - Side effects are at-least-once.
tool_call/connector_callmay run more than once on retry. Keep them idempotent where it matters. - External calls go through tools, not raw hosts. Use
nyxid_proxy(or a typed tool) — never embed a vendor base URL as a direct target. See Accessing external services. - Files are typed inputs.
input_file_refsis not$inputtext and not an interpolation variable. Useforeachwithitems_source: input_file_refsto process multiple files; file tools are still invoked throughtype: tool_call. - Ambient files are fallback inputs. When the caller supplies no explicit
fileRef, the workflow start appends deduplicated ambient file refs from the current tool context. Any explicitfileRefsuppresses ambient refs, so never merge the two sets client-side. An emptyinputsobject is valid when ambient refs exist. Role LLM tool loops receive the same typed refs; do not flatten them into prompt text or reconstruct them from IDs. - Template programs are static and bounded.
transform op: templateaccepts bounded JSON input plus a typedtemplateprogram. The program is not supplied by upstream workflow${...}data. It exposes onlyappend,date,get,json,keys,number, andround; default builtins, CLR access, template loaders, files, and network are unavailable. Limits are 256 KiB for the template, 4 MiB each for input and output, 10,000 loop iterations or mutable-array items, and depth 64. Missing variables, mutation of input data, invalid JSON/template syntax, evaluation errors, and limit violations fail closed.
DSL quick reference
Top-level shape
name: my_workflow # identifier
description: what it does # optional
roles: [ ... ] # actors
steps: [ ... ] # ordered execution
Roles
roles:
- id: analyst # referenced by step.target_role
name: Analyst # optional display name
system_prompt: "You are a strict analyst."
# optional, usually omit and inherit session defaults:
# provider: openai
# temperature: "0.2"
# allowed_tools: [web_search] # ceiling of agent tools this role can see; [] = none
# connectors: [my_api] # whitelist for connector_call
agent_kind defaults to workflow.role-agent. Omit model (see Engine rules). allowed_tools: [] means the role exposes no agent tools.
Step shape
steps:
- id: step_a # unique within the workflow
type: llm_call # primitive type (see table)
target_role: analyst # which role runs it (alias: role); some types need none
parameters: # all values are strings
prompt_prefix: "Analyze:"
next: step_b # explicit successor; omit only on the final step
branches: # for conditional/switch/vote: branch key -> step id
true: step_b
false: step_c
# compensation: undo_step # only tool_call/connector_call/secure_connector_call
# allowed_tools: [web_search] # only llm_call: narrow tool scope (intersection with role)
Reliable-core primitives (prefer these)
llm_call — run the target role's LLM.
- id: analyze
type: llm_call
target_role: analyst
parameters: { prompt_prefix: "Summarize the input:" }
tool_call — call a registered tool (incl. nyxid_proxy, code_execute, document_extract, workflow_file_submit). A JSON-object result is mirrored to steps.<id>.json.<field> for later branching.
- id: fetch
type: tool_call
capability:
nyxid_operation:
user_service_id: <copied-user-service-id>
endpoint_id: <listed-endpoint-id>
parameters:
tool: nyxid_proxy
arguments: '{"query":{}}'
code_execute — run deterministic Python/JavaScript/TypeScript/Bash in the sandbox.
Do not call external services or LLMs from code_execute; use nyxid_proxy for external services and llm_call for LLM work.
- id: build_payload
type: tool_call
parameters:
tool: code_execute
arguments: '{"language":"python","code":"import json\nprint(json.dumps({\"ok\": True}))"}'
document_extract — extract text from one current workflow file ref.
- id: extract_file
type: tool_call
parameters:
tool: document_extract
arguments: "{}"
workflow_file_submit — upload an existing workflow file ref to a NyxID service.
- id: submit_file
type: tool_call
parameters:
tool: workflow_file_submit
arguments: '{"file_ref":{"file_id":"<file-id>","owner_run_id":"<run-id>"},"slug":"my-upload-service","path":"/v1/upload"}'
transform — deterministic data ops from the allowlist above. replace and regex_replace are not implemented; never author them. Unknown names silently behave as identity in the current runtime, so validate the name and the output rather than trusting a green step.
- id: total
type: transform
parameters: { op: group_by, key: category, value: amount, aggregate: sum, precision: "2" }
For deterministic aggregation or JSON/report rendering that exceeds the fixed operations, use the typed bounded template transform. The input must be JSON and is exposed as data:
- id: summarize_items
type: transform
parameters:
op: template
template: >-
{{ total = 0 }}
{{ for item in data.items; total = total + number(item.amount); end }}
{{ json({ count: data.items.size, total: round(total, 2) }) }}
json_parse — parse a JSON string selected by path into structured JSON.
- id: parse_embedded_json
type: transform
parameters:
op: json_parse
path: "$.payload"
assign — write a workflow variable (often the final output step).
- id: finalize
type: assign
parameters: { target: final_summary, value: "$input" }
conditional — two-way branch; set branches.true/branches.false.
switch — multi-way branch on a value; set parameters.branch.<key> and branches, include _default.
- id: route
type: switch
parameters:
on: "${steps.classify.json.category}"
branch.urgent: handle_urgent
branch._default: handle_normal
branches: { urgent: handle_urgent, _default: handle_normal }
foreach — split input by delimiter, run a sub-step per item, merge.
- id: per_item
type: foreach
parameters:
delimiter: "\n"
sub_step_type: llm_call
sub_target_role: worker
sub_param_prompt_prefix: "Process item:"
For multiple workflow input files, use items_source: input_file_refs; each child step receives exactly one current file ref.
- id: extract_each_file
type: foreach
parameters:
items_source: input_file_refs
sub_step_type: tool_call
sub_param_tool: document_extract
sub_param_arguments: "{}"
Keep items_source, sub_step_type, and sub_param_tool under parameters; root-level items_source / sub_param_tool are not reliably lifted by the parser. Use sub_param_arguments: "{}" when the tool should read the per-item file ref instead of treating the file id input as arguments.
Each foreach child expands every sub_param_* value against that child, after fan-out. Use ${input} or ${output} inside sub_param_arguments, paths, or prompts to refer to the current item; both names resolve to the same per-item value at expansion time. Do not pre-expand those fields against the parent input.
Parallelism: concurrent fan-out → merge
Yes — the engine runs branches concurrently, and foreach / parallel / map_reduce / race are the primitives that express it. Each one dispatches its sub-steps in parallel (by default up to 20 at once, hard ceiling 200; set max_concurrent_workers to change the cap and min_concurrent_workers for a steady-state floor). The parent step then waits for all sub-steps and merges their text outputs joined by \n---\n. That fan-out → fan-in is the aevatar equivalent of a tool like n8n where many source branches feed one merge node.
There is no free-form DAG: you do not hand-draw N parallel branches into a merge node. Instead you pick the primitive whose built-in fan-out matches your shape:
| You want… | Use | How each branch is fed |
|---|---|---|
| One input, run by N workers concurrently, then optionally vote a winner | parallel | every worker gets the same $input |
| A list of items, run the same sub-step on each, then concatenate | foreach | the input is split into items; each sub-step gets a different one |
| A list of items, process each, then synthesize all into one result | map_reduce | split → map each (different item) → reduce the merged outputs once |
| N alternative attempts, take the first to finish | race | every branch gets the same $input; first success wins, the rest are discarded |
The n8n "read N sources in parallel → merge" shape is a list → fan-out → merge, so it is foreach (concatenate the per-item results) or map_reduce (synthesize them into one) — not parallel. parallel and race feed the same input to every branch (ensemble / consensus / first-wins), not a different source per branch.
Where the item list comes from (foreach / map_reduce): the previous step's output, split by delimiter (default \n---\n) or parsed as a JSON array; or an explicit items: list; or items_source: input_file_refs (one file per item). Produce that list upstream — the run input, an assign, or transform op: rss_extract_items.
parallel — fan one input out to N llm_call workers, merge (optionally vote). Sub-steps are always llm_call; it needs either workers (distinct roles) or target_role + parallel_count.
- id: critique
type: parallel
parameters:
workers: "reviewer_a,reviewer_b,reviewer_c" # one llm_call per role; each gets the SAME $input
# parallel_count: "3" # alternative: N copies of target_role instead of distinct workers
# max_concurrent_workers: "20" # default 20, ceiling 200
# vote_step_type: vote # optional: aggregate the N outputs via a consensus rule (see the vote primitive)
next: finalize
map_reduce — split into items, map each concurrently, then reduce the merged results into one output. The map phase carries no per-step parameters, so map is best for llm_call analysis driven by the map role's system_prompt; reduce_prompt_prefix is prepended to the merged outputs before the reduce step.
- id: analyze_all
type: map_reduce
parameters:
delimiter: "\n---\n" # how the input splits into items (or pass a JSON array)
map_step_type: llm_call
map_target_role: analyst # analyzes every item concurrently — the fan-out
reduce_step_type: llm_call
reduce_target_role: synthesizer # runs ONCE on the merged map outputs — the fan-in
reduce_prompt_prefix: "Synthesize these analyses into one brief:"
next: finalize
Omit the reduce_* fields and you get just the merged map outputs (no synthesis) — that is equivalent to foreach.
Full primitive vocabulary (use advanced ones only when needed)
| Group | Types |
|---|---|
| AI | llm_call, tool_call, evaluate (score+threshold), reflect |
| Data | transform, assign, retrieve_facts, cache |
| Control | guard/assert, conditional, switch, while/loop, delay/sleep, lease/mutex, wait_signal, checkpoint |
| Composition | foreach, parallel/fan_out, race, map_reduce, workflow_call, dynamic_workflow, vote |
| Integration | connector_call (aliases: http_get, http_post, http_put, http_delete, mcp_call, cli_call), emit/publish |
| Human | human_input, human_approval, wait_signal |
Advanced notes: human_approval/wait_signal suspend the run until a resume/signal event — use them for approvals and long external waits instead of stretching a step past its 600s executor limit. parallel/foreach/map_reduce accept min_concurrent_workers/max_concurrent_workers (see Parallelism: concurrent fan-out → merge above for which one to pick and the concurrency defaults). Side-effecting steps may declare compensation: <step_id> for saga rollback.
Interpolation
$input— the current step's input (the previous step's output, or — for the FIRST step — the run prompt). This is how a value flows step-to-step.${steps.<id>.output}— a prior step's text output. It is.output, NOT.text. The engine registerssteps.<id>.outputand neversteps.<id>.text, so${steps.<id>.text}silently resolves to an empty string — the run still shows every step "completed", but a tool/connector downstream receives an empty argument and fails.${<name>}— a workflow variable written by anassignstep (target: <name>). This is the canonical way to read a captured value back in a later step;${steps.<capture-id>.text}does NOT work (use the bare${<name>}, or equivalently${steps.<capture-id>.output}).${steps.<id>.json.<field>}— a field from a prior step whose output was a JSON object (e.g. atool_callresult). Also:${steps.<id>.success},${steps.<id>.error},${steps.<id>.annotations.<ns>.<key>}.- Expression functions (usable in any value, incl.
condition):if,concat,isblank,length,not,and,or,upper,lower,trim,json,add,sub,mul,div,eq,lt,lte,gt. There is nocontains,replace, regex, or substring function.
Gotchas that silently break runs (verified against the engine — a clean test run does NOT catch these, because failed tool calls return their error as ordinary step output):
${steps.<id>.text}is always empty — use${steps.<id>.output}. This is the #1 cause of "every step completed but the connector got an empty argument."- Read an
assigned value with the bare${<target>}, not${steps.<capture-id>.text}.transform op: splitjoins all parts with\n---\nand ignores anyindex— it is for fan-out, not single-element extraction. To use one segment ofa/b(e.g. anowner/repoin a path), pass the whole string where the/is already correct rather than splitting it apart.conditional.conditionis interpolated first; if the result is not literallytrue/false, the engine does a substring$input.Contains(condition). Since there is nocontainsfunction, build "any/all contain token" checks around this:concatthe inputs into one string in the prior step, then setconditionto the literal token.parallel(andrace) feed every branch the same$inputand each sub-step is always anllm_call. For different input per branch — the "N different sources" shape — split a list withforeach/map_reduceinstead. All four merge sub-step outputs with\n---\n.map_reduce's map sub-steps receive no per-step parameters (drive them via the map role'ssystem_prompt); onlyforeachpassessub_param_*to each child, so per-itemtool_callfetches must useforeach.switch.onthat resolves to empty silently takes the WRONG branch.switchevaluatesonfirst; if it resolves to empty — e.g.${steps.read.json.off_count}when that field is absent, which is exactly what happens when the prior tool returned an error envelope ({"error":true,"status":503,...}) instead of the expected object —switchfalls back to matching the step's whole$input, and matching is exact → substring-contains →_default. So a 503 error body substring-matches a branch key"0"(via"503"/"8001") and quietly routes as if the value were0. Always makeona clean, never-empty token:on: "${eq(steps.read.json.off_count, '0')}"with branches"true"/"false"/_default. Never branch on a raw count/field that can go missing.- A side-effecting workflow MUST read back and assert the real end state — a step "success" does NOT mean the side effect happened. Because a failed tool call returns its error as ordinary step output (above), an action step like
turn_on/post/createcompletes "successfully" even when nothing changed (e.g. the downstream returned 503). Pattern: after the action, add atool_callthat reads the real state (have the upstream API compute a scalar you can branch on — e.g. an HA/templatereturning{"off_count":N}),switchon${eq(<count>,'0')}; on the not-yet-satisfied branchdelay+ re-read for a bounded retry, and on final failure route to aguardwithon_fail: failso the run actually fails (red in the observatory) instead of a green run that did nothing. Do not trust${steps.<action>.success}.
Accessing external services
There is a raw current-turn proxy surface, two compiled workflow capability shapes, and a separate host-connector subsystem. Do not mix their fields.
- Raw current-turn
nyxid_proxy. First select an exact UserService instance. The call requiresservice_id + slug + path; method defaults to GET. Optional fields arebody, non-sensitiveheaders, andresponse_mode:
This is a direct current-turn tool call, not workflow YAML.{ "service_id": "<selected-user-service-id>", "slug": "<service-slug>", "path": "/v1/resource", "method": "POST", "body": {"k": "v"} } - No dynamic current-turn operation tools.
nyxid_service_operation__*andnyxid_service_requestare retired. Never invent or cache one of those tool names. Current-turn management uses the typed connected-service tools; raw one-off HTTP usesnyxid_proxyonly where that surface is explicitly available. - Compiled published operation. Call
list_external_workflow_capabilities, copy the exact descriptorselector, inspect it for the required execution mode, and persist it beside the step ascapability.nyxid_operation.endpoint_idis the authored operation selector;operation_idis reserved/absent from this YAML/proto contract:
Pass that whole object to{ "selector": { "nyx_id_operation": { "user_service_id": "us-lark-7", "endpoint_id": "get-message" } }, "execution_mode": "interactive" }inspect_external_workflow_capability_readiness;nyx_id_operationis required and must not be flattened.
The compiler's admission proof owns UserService identity, slug, internal operation identity, method, path template, digest, schemas, and response policy. Runtime- id: fetch_message type: tool_call capability: nyxid_operation: user_service_id: us-lark-7 endpoint_id: get-message parameters: tool: nyxid_proxy arguments: '{"path_params":{"message_id":"m-42"}}'argumentsmay contain only admittedpath_params,query,headers,body, andresponse_mode. Do not sendservice_id,user_service_id,slug,path,method,endpoint_id,operation_id, or a contract digest inarguments. - Operation discovery is surface-dependent.
list_external_workflow_capabilitiesis an in-session tool, not a public REST endpoint./api/v1/keysproves exact UserService identity but does not replace the workflow operation list. If the client surface exposes neither the list tool nor an operation picker, do not probe invented REST paths or guess an endpoint. Reportoperation discovery unavailableand require an exact selector from an authoritative service contract or a supported surface before binding. - Compiled authored HTTP request. Use
capability.nyxid_requestonly for an exact request contract that cannot be selected as a published operation. Author the typed method, relative path template, allowed request fields, and response policy; select one exact UserService from/api/v1/keys. Binding must preview the current request-contract digest and risk, then obtain the authenticated binder's explicit confirmation/grant. Saving a draft, preview success, or/api/v1/user-servicescannot grant execution. Authored requests never rediscover inventory or OpenAPI at runtime; they execute only the committed proof and matching grant. A step capability is a oneof: do not putnyxid_requestbesidenyxid_operationon the same step. Different steps may use different shapes when each is admitted independently.capability: nyxid_request: user_service_id: us-lark-7 method: GET path_template: /open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id} query_parameters: [user_id_type] body_mode: none response_mode: text - Durable scheduling inspects every call site. A sample input that chooses a preview branch does
not remove a write call site from the definition. On a host without scheduled operation
authority, any authored non-read request makes Team automation preflight fail closed even when
the prompt says
submit:false. To prove scheduling safely, publish a separate preview-only revision with the write steps and their branch targets physically removed. Do not weaken the original workflow or route it through generic scheduling to bypass admission. - Registered workflow connectors. If the capability is a connector registered in the workflow connector registry, call it with
connector_calland authorize it on the role:roles: - id: caller name: Caller connectors: [my_connector] steps: - id: call type: connector_call target_role: caller parameters: { connector: "my_connector", operation: "list", path: "/v1/items", timeout_ms: "10000" } allowed_toolsgotcha. A role with noallowed_toolssees the full inherited tool catalog (includingnyxid_proxy). But the moment you setallowed_toolson a role, you must list every tool its steps call (e.g.allowed_tools: [nyxid_proxy]) — otherwise thetool_callwill not resolve the tool at run time.- Prefer a typed tool when one exists for the capability (they expose stable control fields and validation) over a hand-built proxy path.
- Missing or invisible service. Use
nyxid_require_servicefor an in-session connect/add/authorize request, or the supported NyxID CLI/console flow as a client.USER_SERVICE_NOT_VISIBLEdoes not prove that a simple registration retry will fix the account, and/api/auth/nyxid/authorization-catalog:refreshis reconciliation rather than service registration. If refresh returnsapi_key_scope_plan_route_unresolved, stop and report the catalog blocker; do not delete/reconnect unrelated services or guess which route caused it.org_role_insufficientrequires an authorized organization operator. Never fabricate a slug, connector, or self-service path. - Preflight only the downstream access the workflow actually uses. Binding/readiness does not test provider data ACLs. If a Lark Base workflow reads a record, run one explicitly authorized read-only call against a known sample
record_idwith the same UserService, Base/app token, table, and admitted read contract; require expected fields, not only 2xx.91403means the exact Bot application lacks Base document access;1254302 RolePermNotAllowmeans its advanced-permission role does not cover the target table/action. If a source-verified automation payload already carries every required field and the workflow makes no Base API call, do not add a read capability or demand Base document/role access. This probe proves only the read path it exercises, not trigger authenticity, durable admission, or write authority.
Validating and proving a run
Dispatch one test run with aevatar_start_workflow, passing the draft inline:
{ "workflow_id": "<name>", "workflow_yamls": ["<full yaml>"], "inputs": { "prompt": "<test input>" } }
If the definition consumes structured run input (for example it captures $input and applies
json or feeds it to a transform), inputs.prompt must be a non-empty serialized JSON string,
such as "{\"submit\":false}". Never pass natural-language prose, an empty string, or an
unserialized object to a typed workflow. Free-text workflows that do not declare structured input
keep their normal prompt semantics.
workflow_id is required; inputs is an object (typically { "prompt": "..." }, optionally input_parts / headers). File-bearing calls may carry normalized inlineFile or fileRef parts; member stream invocation and draft-run share the same ingress normalization, so preserve the typed file part instead of flattening it into prompt text.
Interpret evidence in order:
accepted/streamingmeans only that dispatch started.- After acceptance, require the first projection-backed business frame within 30 seconds. SSE
: keepaliveis transport-only and does not satisfy or extend this deadline. If the stream closes with rootRUN_ERROR(code=RUN_OBSERVATION_TIMEOUT), observation timed out, not necessarily the workflow itself; query the sameactorId + commandIdfor later state and never create another run as a status probe. - Only root
RUN_FINISHEDand rootRUN_ERRORare terminal for the workflow stream. Role text, reasoning, tool-call, tool-result, or role-level terminal frames are progress and must not end observation. - A typed parse, admission, identity, or resource-limit error is a structural failure; fix the document before another run.
- For the same run, inspect terminal completion, run detail, and audit. Success requires completed terminal state,
lastSuccess=true, expected non-empty step output, and non-empty final output. - On failure, locate the first failed audit step and classify it before retrying. For NyxID HTTP auth failures, distinguish missing caller credential propagation, missing executor propagation, wrong exact route/credential selection, and downstream UserService authorization.
- Never start another run merely to check whether the previous run finished.
Persisting (make it reusable)
Once the draft dispatches without a parse error, publish a private skill that carries the workflow:
{
"name": "<kebab-case-workflow-name>",
"description": "<one line: what it does>",
"version": "1.0",
"category": "runtime-based",
"instructions_markdown": "Runs the <name> workflow. Invoke with use_skill then aevatar_start_workflow; inputs: <list>.",
"workflow_yamls": [ { "workflow_id": "<name>", "content": "<full yaml>" } ]
}
Choose a clear name/description so the user (and future searches) can find it. Publishing is private by default; the user can later promote it to public on the platform.
Re-run later: the user (or their model) loads it with use_skill("<name>") — which mounts the workflow into their scope — then calls aevatar_start_workflow with workflow_id: "<name>". The run goes through the normal engine path and is visible in the observatory.
Client REST path
When the server-side Aevatar/NyxID tools are absent, read references/client-rest.md before validating, publishing, or invoking through the NyxID broker.
Worked examples
Read references/worked-examples.md when you need complete YAML shapes for branching, per-item calls, typed files, bounded templates, or fan-out/reduce.
Self-check before publishing
- Final step is last in the document and has no
next; every branch reaches it via explicitnext. - Any date/time the logic needs is injected via input, not assumed.
- Typed/structured run input is passed as one non-empty serialized JSON string; schedule and webhook dates come from their documented ingress-time templates and explicit timezone.
- Every externally pushed field has an explicit ingress authority decision; unverified input is treated as a claim/locator, while trusted input is bound to a dedicated authenticated source and the delivery/idempotency contract is explicit.
- No hardcoded
model:unless the user demanded one. - Arithmetic / totals / dedup use
transform, notllm_call. - Every transform
opis on the explicit allowlist; noreplace/regex_replaceor other unknown identity fallback is present, and required transformed output is asserted. - Every workflow external call copied an exact listed
user_service_id + endpoint_idselector (or carries one independently admittednyxid_request), passed readiness for its execution mode, and puts only runtime operation values innyxid_proxy.arguments; raw current-turn proxy calls use exactservice_id + slug + path. - One authorized dispatch was observed to root
RUN_FINISHED/RUN_ERROR, or aRUN_OBSERVATION_TIMEOUTwas reconciled by querying the sameactorId + commandId; no second run was created as a status check. - Any parallel fan-out uses the right primitive: same input →
parallel/race; a list of different items →foreach(concatenate) ormap_reduce(synthesize). Per-itemtool_callfetches useforeach, notmap_reduce.