Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
af01940
Capture the workflow runs setting-off planning golden
paullizer Sep 30, 2026
1501fb1
Add the Run Workflows From Chat setting and its gates
paullizer Sep 30, 2026
fd1e70c
Build the workflow planning context for either workflow capability
paullizer Sep 30, 2026
2c90812
Add the workflow_run capability and its plan checks
paullizer Sep 30, 2026
bb1699e
Force manual approval for plans that start a saved workflow
paullizer Sep 30, 2026
db249c5
Start a saved workflow from a chat plan: the workflow_run adapter
paullizer Sep 30, 2026
5bdff7a
Move Phase 5 to version 0.261.212
paullizer Sep 30, 2026
2430df0
Merge origin/paullizer-react-v2-ui into the Phase 5 branch
paullizer Sep 30, 2026
83c6bd9
Mark external effects on the capability registry
paullizer Oct 1, 2026
4014d30
Cover the receipt path, the session check and every run context
paullizer Oct 1, 2026
2049e66
Say in the reply which saved workflows a plan started
paullizer Oct 1, 2026
23ae2dd
Link the workflow runs a chat plan started (Phase 5, #1551)
paullizer Oct 1, 2026
f497d98
Keep workflow run log codes in Application Insights
paullizer Oct 1, 2026
4ee58f1
Document starting saved workflows from chat (Phase 5, #1551)
paullizer Oct 1, 2026
18a2288
Check the workflow run links route log keeps only hashed ids
paullizer Oct 1, 2026
191cf7f
Pin that the executor never retries a workflow_run step on its own
paullizer Oct 1, 2026
99ca676
Link workflow runs through the reviewed workflowRunHref builder
paullizer Oct 1, 2026
df8bfd0
Clear CodeQL notes in workflow run code
paullizer Oct 1, 2026
678f56c
Pin that the link route refuses a run whose record names another conv…
paullizer Oct 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions application/single_app/admin_settings_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -3779,6 +3779,26 @@
{"key": "allow_user_workflows", "equals": True},
],
},
{
"key": "enable_chat_orchestration_workflow_runs",
"type": "switch",
"label": "Run Workflows From Chat",
"help": (
"When a user asks for it, such as \"run my weekly digest now\", a plan can start "
"one of the user's saved personal workflows from the user's own private "
"conversation. A plan that starts a workflow always waits for the user to run "

Check warning on line 3789 in application/single_app/admin_settings_fields.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
"it, whatever the approval mode, and approving it starts each named workflow "
"once. The answer links to the run; results appear where the workflow already "
"puts them. Only workflows with durable execution on can be started this way. "
"Off by default, because a run does whatever its workflow is set up to do, with "
"the user's access. Requires Chat Orchestration and Enable Personal Workflows."
),
"default": False,
"depends_on": [
{"key": "enable_chat_orchestration", "equals": True},
{"key": "allow_user_workflows", "equals": True},
],
},
{
"key": "chat_orchestration_enabled_capabilities",
"type": "checkbox_set",
Expand All @@ -3790,6 +3810,7 @@
"the content of a file, and Create a file to deliver a downloadable file. "
"Generate images also requires Image Generation. Use an action also "
"requires Enable Action Access. Propose workflows also requires Propose "
"Workflows From Chat and personal workflows. Run workflows also requires Run "
"Workflows From Chat and personal workflows."
),
"default": [],
Expand All @@ -3807,6 +3828,7 @@
{"value": "render_file", "label": "Create a file"},
{"value": "generate_image", "label": "Generate images"},
{"value": "workflow_propose", "label": "Propose workflows"},
{"value": "workflow_run", "label": "Run workflows"},
],
"depends_on": {"key": "enable_chat_orchestration", "equals": True},
},
Expand Down
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.210"
VERSION = "0.261.212"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
15 changes: 15 additions & 0 deletions application/single_app/functions_orchestration_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
CAPABILITY_URL_FETCH,
CAPABILITY_WEB_SEARCH,
CAPABILITY_WORKFLOW_PROPOSE,
CAPABILITY_WORKFLOW_RUN,
DEPENDENCY_PLAN_CONTRACT_VERSION,
DOCUMENT_ACTION_TYPE_COMPARISON,
VISUAL_CHART,
Expand Down Expand Up @@ -1855,6 +1856,19 @@ def run_workflow_propose(step, context, *, settings, user_id, emit=None, cancel_
)


def run_workflow_run(step, context, *, settings, user_id, emit=None, cancel_requested=None):
"""Start one of the user's saved workflows and link to its run.

The workflow runtime loads only when a plan starts a workflow, so resolving this adapter
imports nothing beyond this module.
"""
from functions_orchestration_workflow_runs import adapter_workflow_run

return adapter_workflow_run(
step, context, settings=settings, user_id=user_id, emit=emit, cancel_requested=cancel_requested,
)


# Keyed by capability id, which is also each descriptor's declared ``adapter`` name, so the
# executor can look an adapter up straight from the step's capability without a second map.
ADAPTER_REGISTRY = {
Expand All @@ -1868,6 +1882,7 @@ def run_workflow_propose(step, context, *, settings, user_id, emit=None, cancel_
CAPABILITY_AGENT_INVOKE: run_agent_invoke,
CAPABILITY_ACTION_INVOKE: run_action_invoke,
CAPABILITY_WORKFLOW_PROPOSE: run_workflow_propose,
CAPABILITY_WORKFLOW_RUN: run_workflow_run,
}


Expand Down
46 changes: 46 additions & 0 deletions application/single_app/functions_orchestration_deliverables.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@
CAPABILITY_GENERATE_IMAGE,
CAPABILITY_RENDER_FILE,
CAPABILITY_WORKFLOW_PROPOSE,
CAPABILITY_WORKFLOW_RUN,
MAX_GENERATED_IMAGES_PER_PLAN,
VISUAL_CHART,
VISUAL_DIAGRAM,
VISUAL_IMAGE_PROPOSAL,
VISUAL_KINDS,
WORKFLOW_PROPOSALS_SETTING,
WORKFLOW_RUNS_SETTING,
resolve_admitted_export_catalog,
)
from functions_orchestration_result_contracts import (
Expand Down Expand Up @@ -109,6 +111,18 @@
),
'workflow_one_per_request': 'One request can propose one workflow. Ask for the next one in a new message.',
}
# Why a saved workflow cannot be started from this request. Starting a workflow is not a
# deliverable, so these reach the planner as a fact, and only while an administrator has turned
# starting workflows from chat on.
WORKFLOW_RUN_UNAVAILABLE_REASONS = {
'capability_not_enabled_for_orchestration': 'Starting saved workflows is not enabled for chat orchestration.',
'workflow_runs_disabled': 'Starting saved workflows from chat is turned off for this deployment.',

Check warning on line 119 in application/single_app/functions_orchestration_deliverables.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
'workflow_role_required': 'Your account does not have access to personal workflows.',

Check warning on line 120 in application/single_app/functions_orchestration_deliverables.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
'workflow_shared_conversation': (

Check warning on line 121 in application/single_app/functions_orchestration_deliverables.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
'Saved workflows can be started only from your own conversations, not from shared ones.'

Check warning on line 122 in application/single_app/functions_orchestration_deliverables.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
),
'workflow_context_unavailable': 'Your saved workflows could not be loaded for this request. Try again later.',
}
_KIND_LABELS = {
KIND_ANSWER: 'answer', KIND_FILE: 'file', KIND_IMAGE: 'image',
KIND_CHART: 'chart', KIND_DIAGRAM: 'diagram', KIND_WORKFLOW: 'workflow',
Expand Down Expand Up @@ -186,6 +200,17 @@
return _unavailable('workflow_proposals_disabled')


def _workflow_run_reason(unavailable):
"""The closed reason this caller's plan cannot start a saved workflow."""
reason = unavailable.get(CAPABILITY_WORKFLOW_RUN)
if reason == 'not_enabled_for_orchestration':
return 'capability_not_enabled_for_orchestration'
if isinstance(reason, str) and reason in WORKFLOW_RUN_UNAVAILABLE_REASONS:
return reason
# A settings gate, or a condition this module has no text for, reads as turned off.
return 'workflow_runs_disabled'

Check warning on line 211 in application/single_app/functions_orchestration_deliverables.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.


def _image_options(capabilities):
"""The exact size, quality, and background values the configured image model accepts."""
for capability in capabilities or ():
Expand Down Expand Up @@ -333,6 +358,27 @@
'depends_on and no input bindings) and that delivers the workflow deliverable. When '
'the request also wants a result now, answer it once with the usual steps as well.'
)})
# Starting a saved workflow is described only while an administrator has turned it on;
# otherwise the planner is told exactly what it was before.
if settings.get(WORKFLOW_RUNS_SETTING) is True:
if CAPABILITY_WORKFLOW_RUN in available:
facts.append(
'workflow_run starts one of the user\'s saved workflows after the user approves the plan; '
'a plan that starts one always waits for approval. The server adds to the reply whether '
'each workflow started, so never say that a workflow started, ran or finished, and never '
'promise its results in this conversation.'

Check warning on line 369 in application/single_app/functions_orchestration_deliverables.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
)
recipes.append({'for': 'A saved workflow the user asks to run now', 'steps': (
'one workflow_run step per workflow, naming its durable catalog handle, with no depends_on, '
'no inputs and no deliverable. When the request only starts workflows, declare no '
'deliverables and no final_response; otherwise answer the rest as usual.'
)})
else:
facts.append(
'Starting saved workflows is unavailable for this request. '
f'{WORKFLOW_RUN_UNAVAILABLE_REASONS[_workflow_run_reason(unavailable)]} '
'When the user asks to run a workflow, say why in the answer.'
)
truth.update({'unavailable_reasons': unavailable_reasons, 'facts': facts, 'recipes': recipes})
return truth

Expand Down
18 changes: 17 additions & 1 deletion application/single_app/functions_orchestration_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@
summarize_plan,
)
from functions_orchestration_workflow_context import (
workflow_planning_configured,
workflow_planning_option,
workflow_proposals_configured,
workflow_run_options,
Expand Down Expand Up @@ -699,7 +700,9 @@
raise
self._preparation_stage = "capabilities"
workflow_configured = workflow_proposals_configured(self.settings)
workflow_planning = self.record.get("workflow_planning") if workflow_configured else None
workflow_planning = (
self.record.get("workflow_planning") if workflow_planning_configured(self.settings) else None
)
request_context = build_capability_request_context(
user_id, identity, user_message, agents, actions, allowed_user_urls=allowed_urls,
**workflow_planning_option(workflow_planning),
Expand Down Expand Up @@ -788,6 +791,8 @@
"user_id": user_id, "active_group_ids": seeds.get("active_group_ids") or [],
},
agent_execution_identity=principal, plan_contract_version=2,
attempt_root_run_id=self.record.get("attempt_root_run_id") or self.record["id"],
signed_in_session=getattr(principal, "bridge", None) is not None,

Check warning on line 795 in application/single_app/functions_orchestration_execution.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
**workflow_run_options(
workflow_planning, self.record.get("time_zone") if workflow_configured else None,
),
Expand Down Expand Up @@ -1008,6 +1013,8 @@
},
user_roles=identity.get("user_roles") or [], user_email=principal.email,
user_enable_agents=False, agent_execution_identity=principal,
attempt_root_run_id=self.record.get("attempt_root_run_id") or self.record["id"],
signed_in_session=getattr(principal, "bridge", None) is not None,

Check warning on line 1017 in application/single_app/functions_orchestration_execution.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
plan_contract_version=2, result_service=self.services.results,
task_results={name: TaskResult.from_dict(value) for name, value in tasks.items()},
execution_deadline_at=self.record["execution_deadline_at"],
Expand Down Expand Up @@ -1284,6 +1291,15 @@
)
if notes:
content.append(notes)
# Imported here, like every caller of the run step's module. Deterministic, model-free:
# the saved workflows the plan started, even when it stopped, failed or waits afterwards.
from functions_orchestration_workflow_runs import workflow_run_note

workflow_runs = workflow_run_note(
self.record["plan"], current.get("execution_steps") or [], stopped=status == "cancelled",
)
if workflow_runs:
content.append(workflow_runs)
if status == "waiting":
content.append(build_failure("result_not_ready")["message"])
elif status in {"failed", "cancelled"}:
Expand Down
51 changes: 47 additions & 4 deletions application/single_app/functions_orchestration_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@
)
from functions_orchestration_deliverables import explicit_image_shortfalls
from functions_orchestration_registry import (
CAPABILITY_TABULAR_ANALYZE, CAPABILITY_WORKFLOW_PROPOSE, DEPENDENCY_PLAN_CONTRACT_VERSION,
admitted_export_pairs, get_capability,
CAPABILITY_TABULAR_ANALYZE, CAPABILITY_WORKFLOW_PROPOSE, CAPABILITY_WORKFLOW_RUN,
DEPENDENCY_PLAN_CONTRACT_VERSION, admitted_export_pairs, external_effect_capability_ids, get_capability,
resolve_available_capability_ids,
)
from functions_orchestration_result_contracts import (
Expand Down Expand Up @@ -220,6 +220,9 @@
_DEFAULT_MAX_REPLANS = 2
# Stops that bound time rather than express intent; a step that already finished keeps its result.
_BUDGET_STOP_REASONS = frozenset({'step_timeout', 'run_timeout'})
# Capabilities whose steps may act outside the plan. One that is interrupted, fails or is stopped
# may already have acted, so its step is effects_uncertain. The registry owns the list.

Check warning on line 224 in application/single_app/functions_orchestration_executor.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
_EFFECT_CAPABILITIES = external_effect_capability_ids()


def _now_iso():
Expand Down Expand Up @@ -360,13 +363,23 @@
export_catalog=None,
workflow_planning=None,
time_zone=None,
attempt_root_run_id=None,
signed_in_session=False,
):
self.run_id = run_id
self.plan_id = plan_id
self.conversation_id = conversation_id
self.user_id = user_id
self.turn_index = turn_index
self.attempt_index = attempt_index
# The first attempt's run id, shared by every retry of the plan. A step that starts a saved
# workflow derives its request id from it, so a retry finds the run instead of starting one.
self.attempt_root_run_id = (
attempt_root_run_id if isinstance(attempt_root_run_id, str) and attempt_root_run_id else run_id
)
# Whether the user's signed-in request is running this attempt. A saved workflow is started
# only then, never while a run continues in the background.
self.signed_in_session = signed_in_session is True
if type(plan_contract_version) is not int or plan_contract_version != DEPENDENCY_PLAN_CONTRACT_VERSION:
raise ResultContractError('result_version_unsupported')
self.plan_contract_version = plan_contract_version
Expand Down Expand Up @@ -574,7 +587,7 @@
'checkpoint_available': False,
'reused': False,
'reused_from_run_id': None,
'effects_uncertain': status == STEP_STATUS_RUNNING and step.get('capability_id') in ('agent_invoke', 'action_invoke'),
'effects_uncertain': status == STEP_STATUS_RUNNING and step.get('capability_id') in _EFFECT_CAPABILITIES,
}
if result.get('saved_analyses'):
record['saved_analyses'] = deepcopy(result['saved_analyses'])
Expand All @@ -593,6 +606,9 @@
if step.get('capability_id') == CAPABILITY_WORKFLOW_PROPOSE and isinstance(result.get('workflow_proposal'), dict):
# Server-only: the proposal card and its accept route read it; public_step_record drops it.
record['workflow_proposal'] = deepcopy(result['workflow_proposal'])
if step.get('capability_id') == CAPABILITY_WORKFLOW_RUN and isinstance(result.get('workflow_run'), dict):
# Server-only: the workflow and run ids the run link reads; public_step_record drops it.
record['workflow_run'] = deepcopy(result['workflow_run'])
return record


Expand Down Expand Up @@ -624,6 +640,32 @@
return {**result, 'workflow_proposal': sidecar} if isinstance(sidecar, dict) else result


def _restore_workflow_run(step, context, status, result, *, user_id):
"""Describe again the run a completed workflow_run step started, when its result came back without it.

A retained, recovered or reused result is rebuilt from the task result alone, which carries
the workflow's name and start status but never its ids.
"""
if (
step.get('capability_id') != CAPABILITY_WORKFLOW_RUN or status != STEP_STATUS_COMPLETED
or not isinstance(result, dict) or result.get('task_result') is None
or isinstance(result.get('workflow_run'), dict)
):
return result
try:
from functions_orchestration_workflow_runs import rebuild_workflow_run

sidecar = rebuild_workflow_run(step, context, user_id=user_id, task=result['task_result'])
except Exception as exc:
log_event(
f'{_LOG_PREFIX} A recovered workflow run could not be described.',
level=logging.WARNING,
extra={**_step_log_context(context, step), 'error_type': type(exc).__name__},
)
return result
return {**result, 'workflow_run': sidecar} if isinstance(sidecar, dict) else result


def _persist(persist, record_type, record):
if not callable(persist):
return
Expand Down Expand Up @@ -1470,6 +1512,7 @@
if step['role'] == 'render' and status == STEP_STATUS_COMPLETED:
context.artifacts.extend(deepcopy(result['artifacts']))
result = _restore_workflow_proposal(step, context, status, result, settings=settings, user_id=user_id)
result = _restore_workflow_run(step, context, status, result, user_id=user_id)
record = _step_record(context, step, index, status, result, started_at, completed_at, elapsed)
record.update({
'checkpoint_available': checkpoints is not None
Expand All @@ -1478,7 +1521,7 @@
'token_usage': {} if reused else deepcopy(context.step_token_usage),
'reused': bool(reused),
'reused_from_run_id': (reused.get('provenance') or {}).get('run_id') if reused else None,
'effects_uncertain': step['capability_id'] in ('agent_invoke', 'action_invoke')
'effects_uncertain': step['capability_id'] in _EFFECT_CAPABILITIES
and status in (STEP_STATUS_FAILED, STEP_STATUS_CANCELLED),
})
save(record)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
validate_plan_document_source_kinds,
validate_plan_requirements,
)
from functions_orchestration_workflow_context import workflow_planning_option, workflow_proposals_configured
from functions_orchestration_workflow_context import workflow_planning_configured, workflow_planning_option

TURN_CONTEXT_FIELDS = (
'conversation_id', 'turn_id', 'user_message', 'user_message_id',
Expand Down Expand Up @@ -330,7 +330,7 @@ def _revision_catalogs(
user_id, identity, context.get('resolved_message') or context['user_message'],
agents, actions, allowed_user_urls=revision_allowed_urls(context),
**workflow_planning_option(
context.get('workflow_planning') if workflow_proposals_configured(settings) else None,
context.get('workflow_planning') if workflow_planning_configured(settings) else None,
),
**runtime_options,
)
Expand Down
24 changes: 23 additions & 1 deletion application/single_app/functions_orchestration_plan_revisions.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from functions_orchestration_result_contracts import InputBinding, ResultContractError, ResultRef
from functions_orchestration_schema import (
LEGACY_PLAN_CODE, LEGACY_PLAN_MESSAGE, PlanValidationError, apply_plan_edits, is_legacy_plan,
plan_contract_version, summarize_plan,
plan_approval_floor, plan_contract_version, summarize_plan,
)
from functions_orchestration_timing import initial_execution_deadline

Expand Down Expand Up @@ -963,6 +963,27 @@ def release_plan_revision(claim):
)


def _require_approval_floor(record, plan):
"""Refuse to start a plan whose enabled steps set an approval floor it no longer carries.

``normalize_plan`` saves such a plan as manual, and the run record copies that mode. A saved
plan or run record that reads otherwise was changed after planning, so it is refused rather
than run on its word.
"""
floor = plan_approval_floor(plan)
if floor is None:
return
if any(
not isinstance(approval, dict) or approval.get('mode') != floor['mode']
for approval in (record.get('approval'), plan.get('approval'))
):
raise PlanRevisionError(
'A plan that starts a saved workflow runs only when you run it yourself. This saved plan '
"was changed, so it can't run. Ask again for a new plan.",
code='approval_floor_required',
)


def claim_plan_run(
run_id, user_id, conversation_id, *, plan_id=None, expected_version=None,
edits=None, conversation_context=None, result_alias_resolver=None,
Expand Down Expand Up @@ -1000,6 +1021,7 @@ def claim_plan_run(
contract_version=contract_version, export_catalog=admitted_catalog,
composition_profiles=composition_profiles,
)
_require_approval_floor(record, plan)
started_at = _now()
now = started_at.isoformat()
plan['status'] = 'running'
Expand Down
Loading
Loading