Adding a new AI workflow should not require writing new agent code. In Convilyn, it doesn't — it requires writing a JSON file. The agent graph is fixed; the workflow spec tells the agent what to do.
This post describes the spec schema, how the agent loads it, and the versioning design that makes old workflows reproducible months later.
The Spec Schema
A workflow spec is a JSON document with five groups of fields:
{
// 1. Identity
"spec_id": "goal_lane.job_fit_score",
"version": "2.0.2",
"status": "active", // active | inactive | coming_soon
"variant": "job_fit_score",
"name": "Job Fit Score",
"description_i18n": {
"en": "...", "zh": "...", "ja": "...", "ko": "..."
},
// 2. Input constraints (preflight validation)
"supported_input_formats": ["pdf", "docx", "doc"],
"max_input_size_bytes": 52428800,
"preflight_rules": [
{
"rule_id": "check_has_resume",
"check_type": "file_count",
"params": { "type": "document", "min": 1 },
"error_message": "Please upload at least one document.",
"is_blocking": true
}
],
// 3. Output definition
"output_specs": [
{ "format": "txt", "additional": { "type": "fit_score_report" } }
],
// 4. Workflow instructions (for the agent)
"phases": [
{ "phase": 1, "name": "Resume Parsing", "description": "..." },
{ "phase": 2, "name": "JD Collection", "description": "..." },
{ "phase": 3, "name": "Fit Analysis", "description": "..." },
{ "phase": 4, "name": "Score Report", "description": "..." }
],
// 5. Agent configuration
"agent_config": {
"system_prompt_id": "goal_agent.universal",
"max_iterations": 25,
"min_tools_for_auto": 4,
"tool_progress_milestones": {
"resume_profile_mcp__parse_document": 10,
"resume_profile_mcp__score_fit": 45,
"store_artifact": 85,
"complete_workflow": 97
}
},
"mcp_config": {
"mcp_servers": ["resume-profile-mcp", "text-analysis-mcp"],
"tools": [
"resume-profile-mcp:parse_document",
"resume-profile-mcp:extract_profile",
"resume-profile-mcp:score_fit",
"text-analysis-mcp:extract_keywords"
]
}
}
Phases as Natural Language Instructions
The phases array is the most important field in the spec. It gets injected into the universal agent prompt as {workflow_phases}, giving the LLM structured instructions for what to accomplish and in what order.
# What the universal prompt template contains:
"""
You are executing a multi-phase workflow. Complete each phase in order:
{workflow_phases}
For each phase:
- Call the appropriate tools to gather required information
- Do not proceed to the next phase until the current one is complete
- After all phases, store the result as an artifact and call complete_workflow
"""
# What {workflow_phases} resolves to (from job_fit_score spec):
"""
Phase 1 — Resume Parsing:
Parse the uploaded document and extract the candidate's profile.
Tool: resume-profile-mcp:parse_document → resume-profile-mcp:extract_profile
Phase 2 — JD Collection:
Ask the user for the job description they want to evaluate against.
Phase 3 — Fit Analysis:
Score the candidate's profile against the job description.
Tool: resume-profile-mcp:score_fit
Phase 4 — Score Report:
Generate a structured fit score report and store it as an artifact.
"""
The agent doesn't need hardcoded knowledge of the job_fit_score workflow. The spec provides that knowledge at runtime. The same agent graph executes every workflow.
Preflight Validation
Preflight rules run before the job is submitted to the agent. They validate inputs synchronously, returning errors immediately rather than letting the agent waste 30 seconds discovering bad inputs.
class PreflightValidator:
def validate(
self,
spec: WorkflowSpec,
uploaded_files: list[FileMetadata],
) -> list[PreflightError]:
errors = []
for rule in spec.preflight_rules:
result = self._apply_rule(rule, uploaded_files)
if not result.passed and rule.is_blocking:
errors.append(PreflightError(
rule_id=rule.rule_id,
message=rule.error_message,
))
return errors
# Example rule types:
# file_count: min/max file count of a given type
# file_type: required formats (e.g. "document", "image")
# file_size: max bytes per file
# slot_filled: required user input collected before submission
Blocking rules prevent job submission. Non-blocking rules generate warnings the user can dismiss. This split lets us enforce hard requirements (must have a resume) while surfacing soft suggestions (a cover letter would improve results).
Tool Progress Milestones
Each tool call is mapped to a progress percentage in tool_progress_milestones. When the agent completes that tool, the SSE progress event is emitted with the mapped value.
# Workflow with 5 meaningful tool calls:
"tool_progress_milestones": {
"resume_profile_mcp__parse_document": 10,
"resume_profile_mcp__extract_profile": 25,
"resume_profile_mcp__score_fit": 45,
"text_analysis_mcp__extract_keywords": 65,
"store_artifact": 90,
"complete_workflow": 99
}
# Result: progress jumps 10→25→45→65→90→99 as tools complete
# User sees meaningful progress, not a linear count
The alternative — deriving progress from iteration count — produces misleading feedback. A 120-iteration workflow at iteration 60 is at 50% by count but may be at 85% by actual work done if the remaining iterations are cleanup. Milestones reflect work, not loop count.
Spec Versioning and Reproducibility
Every spec has a version field and a storage_ref. When a job is submitted, the spec version active at that moment is snapshotted into the job record.
class JobRecord:
spec_id: str # "goal_lane.job_fit_score"
spec_version: str # "2.0.2"
spec_snapshot_hash: str # SHA-256 of the spec JSON at submission time
spec_storage_ref: str # immutable archive path in object storage
If we later update a spec — new phases, different tool list, higher iteration limit — jobs submitted under the old version are unaffected. Customer support can replay a historical job against the exact spec it was submitted with, not the current version.
The snapshot hash also detects spec tampering: if the stored hash doesn't match the archived spec at replay time, the job is quarantined.
Lifecycle via Status Field
The status field controls a spec's lifecycle without deleting it:
"active" → visible in catalog, accepts submissions
"inactive" → hidden from catalog, existing jobs continue
"coming_soon" → visible in catalog with locked UI, no submissions
Disabling a workflow doesn't break in-flight jobs. Workflows in coming_soon state appear in the UI to gather interest before engineering resources are committed. The spec exists; the implementation is gated.
