Skip to main content
Every AgentModel requires two behavior functions before you can run a simulation. initial_data_function decides what data each agent starts with. timestep_function decides how agents update each tick. Both receive the AgentModel instance as their only argument, giving them full access to the graph and all parameters. See Agent model for where these functions fit in the simulation lifecycle.

Function signatures

initial_data_function

def initial_data_function(model: AgentModel) -> dict:
    ...
Called once per node during initialize_graph(). The returned dictionary is merged into that node’s attribute store. Every key you return becomes a node attribute accessible later via graph.nodes[node]["key"].
If your function does not return a dictionary, initialize_graph() will fail when it tries to call node.update(initial_data). Always return a dict, even if it contains only one key.

timestep_function

def timestep_function(model: AgentModel) -> None:
    ...
Called once per timestep. It modifies the graph in place and returns nothing. The graph is accessed via model.get_graph() inside the function.

Accessing parameters inside behavior functions

Both functions receive the AgentModel instance, so they can read any parameter with subscript notation:
def initial_data(model):
    low = model["opinion_min"]   # custom parameter set before initialize_graph()
    high = model["opinion_max"]
    return {"opinion": random.uniform(low, high)}

def timestep(model):
    rate = model["influence_rate"]   # read at runtime each tick
    graph = model.get_graph()
    # use rate to update nodes ...
Store anything your functions need as a model parameter rather than a global variable. This keeps simulations self-contained and easy to vary across runs.

Accessing and modifying graph nodes

Call model.get_graph() to get the underlying nx.Graph object. Node data is a dict-like store on each node:
graph = model.get_graph()

# Read a single node's attribute
value = graph.nodes[0]["opinion"]

# Iterate all nodes with their data
for node, data in graph.nodes(data=True):
    print(node, data["opinion"])

# Update a single attribute
graph.nodes[0]["opinion"] = 0.42

# Merge multiple attributes at once
graph.nodes[0].update({"opinion": 0.42, "confidence": 0.9})

Initialization patterns

The simplest pattern: assign each node a random value independently of all other nodes or parameters.
import random

def initial_data(model):
    return {"opinion": random.random()}   # uniform in [0, 1)

model.set_initial_data_function(initial_data)

Timestep patterns

Each agent picks a random neighbor and averages their values. This is the canonical opinion dynamics pattern.
import random

def timestep(model):
    graph = model.get_graph()
    updates = {}

    for node in graph.nodes():
        neighbors = list(graph.neighbors(node))
        if not neighbors:
            continue
        partner = random.choice(neighbors)
        avg = (graph.nodes[node]["opinion"] + graph.nodes[partner]["opinion"]) / 2
        updates[node] = avg

    # Write all updates after computing them
    for node, opinion in updates.items():
        graph.nodes[node]["opinion"] = opinion

model.set_timestep_function(timestep)

Common pitfalls

The AgentModel instance does not expose the graph directly. You must call model.get_graph() to retrieve it:
# Wrong — AgentModel has no .nodes attribute
def timestep(model):
    for node in model.nodes():   # AttributeError
        ...

# Correct
def timestep(model):
    graph = model.get_graph()
    for node in graph.nodes():
        ...
Writing values back to the graph inside the same loop that reads them causes agents processed later in the tick to see already-updated values from earlier agents. This introduces an ordering bias that is usually unintended.
# Wrong — later nodes see updated opinions from earlier nodes
def timestep(model):
    graph = model.get_graph()
    for node in graph.nodes():
        neighbors = list(graph.neighbors(node))
        avg = sum(graph.nodes[n]["opinion"] for n in neighbors) / len(neighbors)
        graph.nodes[node]["opinion"] = avg   # written immediately

# Correct — compute all new values first, then write
def timestep(model):
    graph = model.get_graph()
    updates = {}
    for node in graph.nodes():
        neighbors = list(graph.neighbors(node))
        avg = sum(graph.nodes[n]["opinion"] for n in neighbors) / len(neighbors)
        updates[node] = avg
    for node, opinion in updates.items():
        graph.nodes[node]["opinion"] = opinion
Collect new values into a separate dict (as shown above) before writing any of them back. This ensures every agent’s update is based on the state at the start of the tick.
Emergent calls node.update(initial_data) on the return value. If your function returns None or any non-dict type, initialize_graph() will raise a TypeError.
# Wrong — returns a number, not a dict
def initial_data(model):
    return random.random()

# Correct
def initial_data(model):
    return {"opinion": random.random()}

Build docs developers (and LLMs) love