AI-operated service with Pack¶
The service owner story where the AI does the rendering: consumers declare what they want, NetOrca Pack's processors turn each declaration into deployable configuration and verify it, and your automation only executes and reports back. When a deployment fails, you feed the error back and the AI fixes its own rendering. Concepts and the module map live in the Pack and AI guide; this page is the workflow.
The running example is a real one: a VIRTUAL_SERVER service whose consumers declare
{name, vip, backend, waf} and whose config processor renders F5 AS3 (the modern successor of
the WAF pack demo, which drove these same endpoints with a hand-rolled REST client - every raw
requests call there is now a module task).
Personas: platform admin (once), service owner (you), consumer (unchanged - they just declare).
Stage 0 - provision the AI stack (once per service)¶
One prerequisite lives outside Ansible by design: an admin configures the LLM models in the GUI's Platform Settings (the platform accepts model writes only from superuser sessions - see the guide). Everything else is declarative:
- name: Provision the AI stack for VIRTUAL_SERVER
hosts: localhost
gather_facts: false
vars:
service_id: 49
tasks:
- name: Which LLM models does the platform offer?
netautomate.netorca.netorca_llm_model_info:
register: models
- name: Pick one by name
ansible.builtin.set_fact:
llm_id: "{{ (models.llm_models | selectattr('name', 'eq', 'Claude 4.6') | first).id }}"
- name: Knowledge the AI should render against
netautomate.netorca.netorca_ai_document:
service_id: "{{ service_id }}"
filename: "{{ item | basename }}"
raw_content: "{{ lookup('ansible.builtin.file', item) }}"
loop: "{{ lookup('ansible.builtin.fileglob', 'knowledge/*.md', wantlist=true) }}"
- name: The config processor - declaration in, AS3 out
netautomate.netorca.netorca_ai_processor:
service_id: "{{ service_id }}"
action_type: config
name: vip_config
llm_model: "{{ llm_id }}"
prompt: "{{ lookup('ansible.builtin.file', 'prompts/config_prompt.md') }}"
response_schema: "{{ lookup('ansible.builtin.file', 'prompts/config_schema.json') | from_json }}"
extra_data:
enable_pack_context: true
active: true
- name: The verify processor - checks the rendering before anyone deploys it
netautomate.netorca.netorca_ai_processor:
service_id: "{{ service_id }}"
action_type: verify
name: vip_verify
llm_model: "{{ llm_id }}"
prompt: "Verify the AS3 against the declaration; approve only exact intent matches."
active: true
- name: Turn Pack on
netautomate.netorca.netorca_pack_profile:
service_id: "{{ service_id }}"
pack_enabled: true
Every task above is an idempotent upsert - re-running the play converges, and changing a prompt
file in Git changes exactly that processor. The runnable version (with an optional auto-approval
validator) is examples/scenarios/pack_ai/provision_ai_stack.yml.
Stage 1 - runs happen¶
From here, pipelines appear without you: a consumer's merge submits a declaration, the change
instance is approved (by your validation playbook,
a human, or a validator processor with allow_auto_approval), and the platform triggers
config -> verify. Each run is a pipeline with a version, a state
(WAITING_FOR_RESPONSE -> OK / FAILED) and the accumulated LLM cost. Manual runs and
re-renders use netorca_pack_trigger - see the
trigger-then-poll pattern.
Stage 2 - the executor loop¶
Successful, not-yet-applied pipelines are your work queue. Deploy what the AI rendered, report the result into the run, mark it applied:
- name: Pack executor loop
hosts: localhost
gather_facts: false
tasks:
- name: Runs waiting for an executor
netautomate.netorca.netorca_pack_pipeline_info:
state: [OK]
applied: false
register: queue
- name: Execute each run
ansible.builtin.include_tasks: tasks/execute_one.yml
loop: "{{ queue.pack_pipelines }}"
loop_control:
loop_var: pipeline
label: "pipeline #{{ pipeline.id }} v{{ pipeline.version }}"
with tasks/execute_one.yml as the block/rescue unit - the same honesty rule as the
classic fulfilment loop: success
and failure both get reported, just into the pipeline instead of the change instance:
- block:
- name: Deploy the rendered config (your infrastructure step)
ansible.builtin.uri:
url: "https://{{ bigip }}/mgmt/shared/appsvcs/declare"
method: POST
body: "{{ pipeline.config.data.as3_json }}"
body_format: json
force_basic_auth: true
url_username: "{{ bigip_user }}"
url_password: "{{ bigip_password }}"
- name: Report success into the run
netautomate.netorca.netorca_pack_data:
object_id: "{{ pipeline.config.object_id }}"
stage: execution
data:
success: true
deployed_at: "{{ now(utc=true).isoformat() }}"
- name: Off the queue
netautomate.netorca.netorca_pack_pipeline:
id: "{{ pipeline.id }}"
applied: true
rescue:
- name: Report the failure into the run
netautomate.netorca.netorca_pack_data:
object_id: "{{ pipeline.config.object_id }}"
stage: execution
data:
success: false
error: "{{ ansible_failed_result.msg | default('unknown error') }}"
Runnable version: examples/scenarios/pack_ai/executor_loop.yml
(writes gated behind -e apply_changes=true, so it is safe to point at a shared instance).
Stage 3 - self-healing: retrigger with feedback¶
When execution failed because the rendering was wrong, don't fix the config by hand - tell the AI and let it re-render:
- name: Ask the AI to fix its own output
netautomate.netorca.netorca_pack_trigger:
object_id: "{{ pipeline.config.object_id }}"
retrigger: true
comment: "deployment failed: {{ ansible_failed_result.msg }} - render against vlan 210"
The retriggered run restarts at config with your comment in the prompts, producing a new
pipeline version; your executor loop picks it up on the next pass. This trigger-execute-feedback
cycle is the pack equivalent of the WAF demo's tuning loop, where Splunk events and BIG-IP
suggestions flowed back into every re-render.
Operating notes¶
- Watch the spend: every pipeline carries its
cost. A scheduled report is one task -netorca_pack_pipeline_infowithstart_date, then sumcostover the results. - State machine:
WAITING_FOR_RESPONSEis normal both mid-run and at an external execution stage; onlyFAILEDneeds attention.SCHEDULEDshows up for crontab-scheduled processors. - Consumers see none of this - their experience stays the GitOps flow; Pack only changes who (or what) does the owner-side work.
- What the modules replaced: the original pack repos vendored a raw REST client
(
netorca_pack_client.py) for stage reads, pushes and retriggers. The executor loop above is that client's whole job, in four declarative tasks, with check mode and structured errors.