Skip to content

Designing Your First Workflow

This tutorial teaches you to load a workflow graph, understand its nodes and edges, run BFS traversal, and see the persona activation order.

What is a Workflow Graph?

A workflow graph defines how FCC personas collaborate. It consists of:

  • Nodes: Personas that participate in the workflow
  • Edges: Directed connections between personas, carrying artifacts or feedback
  • Metadata: ID, title, and description of the workflow

Workflow graphs are stored as JSON files in data/workflows/.

Loading the Base Workflow Graph

from fcc.workflow.graph import WorkflowGraph

graph = WorkflowGraph.from_json("data/workflows/base_sequence.json")

print(f"ID: {graph.meta.id}")
print(f"Title: {graph.meta.title}")
print(f"Description: {graph.meta.description}")
print(f"Nodes: {len(graph)}")
print(f"Edges: {len(graph.edges)}")

Output:

ID: fcc_base_sequence
Title: FCC Personas Collaboration - Base Sequence
Description: Canonical FCC workflow graph: 5 personas, handoffs and feedback loops.
Nodes: 5
Edges: 11

Understanding Nodes

Each node represents a persona in the workflow:

for node in graph.nodes:
    print(f"  {node.id}: {node.name} (type: {node.type})")

Output:

  RC: Research Crafter (type: persona)
  BC: Blueprint Crafter (type: persona)
  DE: Documentation Evangelist (type: persona)
  RB: Runbook Crafter (type: persona)
  UG: User Guide Crafter (type: persona)

Access individual nodes:

rc_node = graph.get_node("RC")
print(f"Node: {rc_node.name}")

# Check if a node exists
print("RC" in graph)  # True
print("CIA" in graph)  # False (not in base workflow)

Understanding Edges

Edges are directed connections between nodes. Each edge has:

  • from_id: The source node
  • to_id: The destination node
  • label: What flows along this edge
  • type: Either handoff (forward flow) or feedback (iterative refinement)
# List all edges
for edge in graph.edges:
    arrow = "-->" if edge.type == "handoff" else "-.>"
    print(f"  {edge.from_id} {arrow} {edge.to_id} [{edge.label}]")

Output:

  RC --> BC [research_inventory]
  BC --> DE [blueprints_specs]
  DE -.> BC [standards_edits]
  DE --> RB [publish_handoff]
  DE --> UG [publish_handoff]
  RB -.> BC [operational_feedback]
  RB -.> RC [operational_findings]
  UG -.> BC [usability_feedback]
  UG -.> RC [user_feedback]
  BC --> RB [updated_specs]
  BC --> UG [updated_specs]

Filtering Edges by Type

# Get only handoff edges (forward flow)
handoffs = graph.handoffs()
print(f"Handoff edges: {len(handoffs)}")
for h in handoffs:
    print(f"  {h.from_id} -> {h.to_id}: {h.label}")

# Get only feedback edges (iterative refinement)
feedbacks = graph.feedbacks()
print(f"Feedback edges: {len(feedbacks)}")
for f in feedbacks:
    print(f"  {f.from_id} -> {f.to_id}: {f.label}")

Querying Node Relationships

Find which nodes are connected to a given node:

# Who does BC send artifacts to?
successors = graph.successors("BC")
print(f"BC sends to: {[n.id for n in successors]}")
# ['DE', 'RB', 'UG']

# Who sends artifacts to BC?
predecessors = graph.predecessors("BC")
print(f"BC receives from: {[n.id for n in predecessors]}")
# ['RC', 'DE', 'RB', 'UG']

# What edges leave BC?
outgoing = graph.outgoing_edges("BC")
for e in outgoing:
    print(f"  BC -> {e.to_id}: {e.label} ({e.type})")

# What edges arrive at BC?
incoming = graph.incoming_edges("BC")
for e in incoming:
    print(f"  {e.from_id} -> BC: {e.label} ({e.type})")

BFS Traversal

BFS (Breadth-First Search) traversal shows the order in which personas are activated when starting from a given node:

# BFS from RC (the typical start node)
order = graph.bfs_from("RC")
print(f"BFS from RC: {order}")

Output:

BFS from RC: ['RC', 'BC', 'DE', 'RB', 'UG']

This tells you: starting from RC, the simulation first activates RC, then BC (via research_inventory), then DE (via blueprints_specs), then RB and UG (via publish_handoff).

Try starting from a different node:

# BFS from DE (starting at the Critique phase)
order = graph.bfs_from("DE")
print(f"BFS from DE: {order}")
# ['DE', 'BC', 'RB', 'UG', 'RC']

Topological Order

Topological order processes nodes in dependency order, ignoring feedback edges. This shows the "natural" processing sequence:

topo = graph.topological_order()
print(f"Topological order: {topo}")

Output:

Topological order: ['RC', 'BC', 'DE', 'RB', 'UG']

The difference between BFS and topological order:

  • BFS follows all edges (handoff and feedback), visiting each node once
  • Topological order only considers handoff edges, producing a dependency-respecting sequence

In the base graph, both happen to produce the same order. In larger graphs with more complex edge patterns, they can differ.

Running a Simulation on This Graph

Connect the workflow graph to the simulation engine:

from fcc.simulation.engine import SimulationEngine

engine = SimulationEngine(graph=graph, max_steps=50)
history = engine.run(start_node="RC", initial_payload="Document the auth API")

print(f"Events: {len(history.events)}")
for event in history.events[:3]:
    print(f"  Step {event['step']}: {event['actor']} (via {event['edge_label']})")

Validated Loading

For production use, validate the workflow graph against its JSON schema:

graph = WorkflowGraph.from_json_validated(
    json_path="data/workflows/base_sequence.json",
    schema_path="data/schemas/workflow.schema.json",
)
print(f"Validated graph: {graph.meta.title}")

This raises a jsonschema.ValidationError if the graph does not conform to the schema.

Serialization

Save a workflow graph back to JSON:

# Serialize to dict
data = graph.to_dict()

# Write to file
graph.to_json("output/my_workflow.json", indent=2)

Next Steps