I've done this in two different ways:
Option (A): Write the dot file from another script. This is particularly useful when I'm using a script (in, say, Python or Perl) to rework the input data into dot format for drawing. In that case, as well as having the Python script write the data into dot format I can also have it write the attributes for each node and edge into the dot file. An example is shown below (not runnable because I've extracted it from a larger script that interprets the input data but you can see how the Perl is writing the dot code).
print "graph G {\n graph [overlap = scale, size = \"10,10\"]; node [fontname = \"Helvetica\", fontsize = 9]\n";
for ($j = 0; $j <= $#sectionList; $j++) {
print "n$j [label = \"$sectionList[$j]\", style = filled, fillcolor = $groupColour{$group{$sectionList[$j]}} ]\n";
}
for ($j = 0; $j <= $#sectionList; $j++) {
for ($i = $j+1; $i <= $#sectionList; $i++) {
$wt = ($collab{$sectionList[$j]}{$sectionList[$i]}+0)/
($collab{$sectionList[$j]}{$sectionList[$j]}+0);
if ($wt > 0.01) {
print "n$j -- n$i [weight = $wt, ";
if ($wt > 0.15) {
print "style = bold]\n";
}
elsif ($wt > 0.04) {
print "]\n";
} else {
print "style = dotted]\n";
}
}
}
print "\n";
}
print "}\n";
Option (B): If I'm writing the dot script by hand, I'll use a macro processor to define common elements. For example given the file polygon.dot.m4 containing the m4 macro define() as follows:
define(SHAPE1,square)
define(SHAPE2,triangle)
digraph G {
a -> b -> c;
b -> d;
a [shape=SHAPE1];
b [shape=SHAPE2];
d [shape=SHAPE1];
e [shape=SHAPE2];
}
... the command m4 <polygon.dot.m4 | dot -Tjpg -opolygon.jpg produces:

Changing the definitions of SHAPE1 and SHAPE2 at the top of the file will change the shapes drawn for each of the relevant nodes.