diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -5,9 +5,7 @@
 ## Getting Started
 
 ```
-cabal sandbox init
-cabal configure --enable-tests
-cabal install --only-dependencies --enable-tests
+cabal install dataflow@0.6.0.0
 ```
 
 ## Usage
@@ -16,60 +14,60 @@
 
 The objects supported by DataFlow is:
 
-* `TrustBoundary`
-* `InputOutput`
-* `Function`
-* `Database`
-* `Flow`
-
-These are composed in a `Diagram` to get something printable.
+* `boundary`
+* `io`
+* `function`
+* `database`
+* `->`
 
-For more on Haskell data types, see [the Hackage site](https://hackage.haskell.org/package/dataflow).
+These are composed in a `diagram` to get something printable.
 
 ## Example
 
-```haskell
-module Main where
-
-import DataFlow.Core
-import DataFlow.Graphviz.Renderer
-import DataFlow.DFD
-
-main :: IO ()
-main = putStr $ renderGraphviz $ asDFD  $
-  Diagram (Just "Webapp") [
-    TrustBoundary "browser" "Browser" [
-      Function "client" "Client"
-    ],
-    TrustBoundary "aws" "Amazon AWS" [
-      Function "server" "Web Server",
-      Database "logs" "Logs"
-    ],
-    InputOutput "analytics" "Google Analytics",
-
-    Flow "client" "server" "Request /" "",
-    Flow "server" "logs" "Log" "User IP",
-    Flow "server" "client" "Response" "User Profile",
+```
+diagram 'Webapp' {
+  boundary 'Browser' {
+    function client 'Client'
+  }
+  boundary 'Amazon AWS' {
+    function server 'Web Server'
+    database logs 'Logs'
+  }
+  io analytics 'Google<br/>Analytics'
 
-    Flow "client" "analytics" "Log" "Page Navigation"
-  ]
+  client -> server 'Request /' ''
+  server -> logs 'Log' 'User IP'
+  server -> client 'Response' 'User Profile'
+  client -> analytics 'Log' 'Page Navigation'
+}
 ```
 
 Then generate your output with dot.
 
 ```bash
-runhaskell example.hs | dot -Tsvg > example.svg
+dataflow dfd webapp.flow | dot -Tsvg > webapp.svg
 ```
 
 That should generate something like the following.
 
 ![Example Output](https://rawgit.com/owickstrom/dataflow/master/examples/webapp.svg)
 
+## Build
+
+```
+cabal sandbox init
+cabal configure --enable-tests
+cabal install --only-dependencies --enable-tests
+```
+
 ## Building the Examples
 
 ```bash
 make -C examples
 ```
+## Haskell Docs
+
+See [the Hackage site](https://hackage.haskell.org/package/dataflow).
 
 ## Release
 
diff --git a/cli/Main.hs b/cli/Main.hs
new file mode 100644
--- /dev/null
+++ b/cli/Main.hs
@@ -0,0 +1,26 @@
+module Main where
+
+import System.Environment
+import DataFlow.Core
+import DataFlow.Reader
+import DataFlow.DFD
+import DataFlow.Graphviz.Renderer
+
+data Command = DFD | Lint
+
+usage :: String
+usage = "Usage: dataflow (dfd|lint) FILE"
+
+dfd :: FilePath -> IO ()
+dfd path = do
+  res <- readDiagramFile path
+  case res of
+    (Left err) -> print err
+    (Right diagram) -> putStr $ renderGraphviz $ asDFD diagram
+
+main :: IO ()
+main = do
+  args <- getArgs
+  case args of
+    ["dfd", path] -> dfd path
+    _ -> fail usage
diff --git a/dataflow.cabal b/dataflow.cabal
--- a/dataflow.cabal
+++ b/dataflow.cabal
@@ -1,5 +1,5 @@
 name:                dataflow
-version:             0.5.4.0
+version:             0.6.0.0
 synopsis:            Generate Graphviz documents from a Haskell representation.
 description:         Outputs .dot files that can be processed by the dot
                      command. Currently it only supports the DFD output format
@@ -17,9 +17,9 @@
 build-type:          Simple
 extra-source-files:  README.md,
                      LICENSE,
-                     examples/webapp.hs,
+                     examples/webapp.flow,
                      examples/webapp.svg,
-                     examples/legend.hs,
+                     examples/legend.flow,
                      examples/legend.svg,
                      examples/Makefile
 cabal-version:       >=1.10
@@ -31,6 +31,7 @@
 library
   exposed-modules:
     DataFlow.Core,
+    DataFlow.Reader,
     DataFlow.Graphviz,
     DataFlow.Graphviz.EdgeNormalization,
     DataFlow.Graphviz.Renderer,
@@ -38,8 +39,17 @@
   build-depends:
     base >=4 && <4.8,
     mtl >=2.2,
-    containers >= 0.4
+    containers >= 0.4,
+    parsec
   hs-source-dirs:      src
+  default-language:    Haskell2010
+
+executable dataflow
+  main-is: Main.hs
+  build-depends:
+    base >=4 && <4.8,
+    dataflow
+  hs-source-dirs:      cli
   default-language:    Haskell2010
 
 test-suite spec
diff --git a/examples/Makefile b/examples/Makefile
--- a/examples/Makefile
+++ b/examples/Makefile
@@ -1,9 +1,13 @@
-SOURCES=$(shell find *.hs)
-TARGETS=$(SOURCES:%.hs=%.svg)
+SOURCES=$(shell find *.flow)
+TARGETS=$(SOURCES:%.flow=%.svg)
+DATAFLOW=../dist/build/DataFlow/dataflow
 
-%.svg: %.hs
+$(DATAFLOW):
+	cd .. && cabal build
+
+%.svg: %.flow $(DATAFLOW)
 	@echo "$< -> $@"
-	@runhaskell -i../src $< | dot -Tsvg > $@
+	@$(DATAFLOW) dfd $< | dot -Nfontname=Courier -Efontname=Courier -Tsvg > $@
 
 svg: $(TARGETS)
 
diff --git a/examples/legend.flow b/examples/legend.flow
new file mode 100644
--- /dev/null
+++ b/examples/legend.flow
@@ -0,0 +1,11 @@
+diagram '' {
+  boundary 'boundary' {
+    function f 'function'
+    database d 'database'
+    io i 'io'
+  }
+
+  f -> d 'Data' 'Description'
+  i -> f '' 'Description'
+  f -> i 'Data' ''
+}
diff --git a/examples/legend.hs b/examples/legend.hs
deleted file mode 100644
--- a/examples/legend.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module Main where
-
-import DataFlow.Core
-import DataFlow.Graphviz.Renderer
-import DataFlow.DFD
-
-main :: IO ()
-main = putStr $ renderGraphviz $ asDFD $
-  Diagram Nothing [
-    TrustBoundary "trust" "TrustBoundary" [
-      Function "function" "Function",
-      Database "database" "Database",
-      InputOutput "io" "InputOutput"
-    ],
-    Flow "function" "io" "Flow" "",
-    Flow "io" "function" "Flow" "(backwards)",
-    Flow "function" "database" "Flow" ""
-  ]
-
diff --git a/examples/legend.svg b/examples/legend.svg
--- a/examples/legend.svg
+++ b/examples/legend.svg
@@ -3,53 +3,52 @@
  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 <!-- Generated by graphviz version 2.38.0 (20140413.2041)
  -->
-<!-- Title: Untitled Pages: 1 -->
-<svg width="406pt" height="308pt"
- viewBox="0.00 0.00 406.00 308.32" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 304.316)">
-<title>Untitled</title>
-<polygon fill="white" stroke="none" points="-4,4 -4,-304.316 402,-304.316 402,4 -4,4"/>
-<g id="clust1" class="cluster"><title>cluster_trust</title>
-<polygon fill="none" stroke="#595959" stroke-dasharray="5,2" points="8,-8 8,-292.316 390,-292.316 390,-8 8,-8"/>
-<text text-anchor="start" x="166.393" y="-281.316" font-family="Arial" font-style="italic" font-size="10.00" fill="#595959">TrustBoundary</text>
+<!-- Pages: 1 -->
+<svg width="313pt" height="357pt"
+ viewBox="0.00 0.00 313.00 356.62" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 352.624)">
+<polygon fill="white" stroke="none" points="-4,4 -4,-352.624 309,-352.624 309,4 -4,4"/>
+<g id="clust1" class="cluster"><title>cluster_boundary</title>
+<polygon fill="none" stroke="#595959" stroke-dasharray="5,2" points="8,-8 8,-340.624 297,-340.624 297,-8 8,-8"/>
+<text text-anchor="start" x="131.65" y="-329.624" font-family="Arial" font-style="italic" font-size="10.00" fill="#595959">boundary</text>
 </g>
-<!-- function -->
-<g id="node1" class="node"><title>function</title>
-<ellipse fill="none" stroke="black" cx="208" cy="-217.908" rx="46.8172" ry="46.8172"/>
-<text text-anchor="start" x="181.152" y="-215.758" font-family="Arial" font-weight="bold" font-size="14.00">Function</text>
+<!-- f -->
+<g id="node1" class="node"><title>f</title>
+<ellipse fill="none" stroke="black" cx="155" cy="-166.562" rx="52.6248" ry="52.6248"/>
+<text text-anchor="start" x="121.395" y="-163.362" font-family="Courier,monospace" font-weight="bold" font-size="14.00">function</text>
 </g>
-<!-- database -->
-<g id="node2" class="node"><title>database</title>
-<text text-anchor="start" x="58.5347" y="-62.3" font-family="Arial" font-weight="bold" font-size="14.00">Database</text>
-<polyline fill="none" stroke="black" points="53.5,-51.5 122.5,-51.5 "/>
-<polyline fill="none" stroke="black" points="122.5,-77.5 53.5,-77.5 "/>
+<!-- i -->
+<g id="node3" class="node"><title>i</title>
+<polygon fill="none" stroke="black" stroke-width="2" points="179,-56 139,-56 139,-16 179,-16 179,-56"/>
+<text text-anchor="start" x="150.599" y="-32.8" font-family="Courier,monospace" font-weight="bold" font-size="14.00">io</text>
 </g>
-<!-- function&#45;&gt;database -->
-<g id="edge3" class="edge"><title>function&#45;&gt;database</title>
-<path fill="none" stroke="black" d="M169.293,-191.395C155.216,-180.751 139.998,-167.488 128.664,-153 114.295,-134.632 103.528,-110.364 96.6951,-92.0459"/>
-<polygon fill="black" stroke="black" points="99.9655,-90.7966 93.3059,-82.5564 93.3733,-93.151 99.9655,-90.7966"/>
-<text text-anchor="start" x="129" y="-140.299" font-family="Arial" font-weight="bold" font-size="12.00" fill="#3184e4">(3) </text>
-<text text-anchor="start" x="147" y="-140.299" font-family="Arial" font-weight="bold" font-size="12.00">Flow</text>
+<!-- f&#45;&gt;i -->
+<g id="edge2" class="edge"><title>f&#45;&gt;i</title>
+<path fill="none" stroke="black" d="M109.247,-140.496C83.3903,-122.922 59.1313,-98.3119 74.9893,-74 86.8169,-55.8669 109.748,-46.5597 128.668,-41.8175"/>
+<polygon fill="black" stroke="black" points="129.651,-45.1851 138.67,-39.6256 128.153,-38.3474 129.651,-45.1851"/>
+<text text-anchor="start" x="92.603" y="-87.4" font-family="Courier,monospace" font-weight="bold" font-size="12.00" fill="#3184e4">(2) </text>
+<text text-anchor="start" x="74" y="-77" font-family="Courier,monospace" font-size="10.00">Description</text>
 </g>
-<!-- io -->
-<g id="node3" class="node"><title>io</title>
-<polygon fill="none" stroke="black" stroke-width="2" points="337.5,-113 240.5,-113 240.5,-16 337.5,-16 337.5,-113"/>
-<text text-anchor="start" x="252.917" y="-62.3" font-family="Arial" font-weight="bold" font-size="14.00">InputOutput</text>
+<!-- f&#45;&gt;i -->
+<g id="edge3" class="edge"><title>f&#45;&gt;i</title>
+<path fill="none" stroke="black" d="M156.92,-103.866C157.459,-86.521 158.007,-68.9241 158.405,-56.1386"/>
+<polygon fill="black" stroke="black" points="153.417,-103.908 156.604,-114.012 160.413,-104.126 153.417,-103.908"/>
+<text text-anchor="start" x="157" y="-82.4" font-family="Courier,monospace" font-weight="bold" font-size="12.00" fill="#3184e4">(3) </text>
+<text text-anchor="start" x="185.805" y="-82.4" font-family="Courier,monospace" font-weight="bold" font-size="12.00">Data</text>
 </g>
-<!-- function&#45;&gt;io -->
-<g id="edge1" class="edge"><title>function&#45;&gt;io</title>
-<path fill="none" stroke="black" d="M191.196,-174.107C188.156,-159.768 187.808,-144.107 194.664,-131 202.93,-115.197 216.936,-102.441 231.662,-92.5323"/>
-<polygon fill="black" stroke="black" points="233.794,-95.3258 240.378,-87.0254 230.054,-89.408 233.794,-95.3258"/>
-<text text-anchor="start" x="195" y="-140.299" font-family="Arial" font-weight="bold" font-size="12.00" fill="#3184e4">(1) </text>
-<text text-anchor="start" x="213" y="-140.299" font-family="Arial" font-weight="bold" font-size="12.00">Flow</text>
+<!-- d -->
+<g id="node2" class="node"><title>d</title>
+<text text-anchor="start" x="121.895" y="-291.925" font-family="Courier,monospace" font-weight="bold" font-size="14.00">database</text>
+<polyline fill="none" stroke="black" points="116.5,-283.125 193.5,-283.125 "/>
+<polyline fill="none" stroke="black" points="193.5,-307.125 116.5,-307.125 "/>
 </g>
-<!-- function&#45;&gt;io -->
-<g id="edge2" class="edge"><title>function&#45;&gt;io</title>
-<path fill="none" stroke="black" d="M241.242,-170.972C245.053,-165.037 248.737,-158.952 252,-153 258.898,-140.418 265.381,-126.282 270.934,-113.101"/>
-<polygon fill="black" stroke="black" points="238.183,-169.252 235.599,-179.527 244.027,-173.106 238.183,-169.252"/>
-<text text-anchor="start" x="267.561" y="-144.4" font-family="Arial" font-weight="bold" font-size="12.00" fill="#3184e4">(2) </text>
-<text text-anchor="start" x="285.561" y="-144.4" font-family="Arial" font-weight="bold" font-size="12.00">Flow</text>
-<text text-anchor="start" x="262" y="-134" font-family="Arial" font-size="10.00">(backwards)</text>
+<!-- d&#45;&gt;f -->
+<g id="edge1" class="edge"><title>d&#45;&gt;f</title>
+<path fill="none" stroke="black" d="M155,-277.056C155,-264.647 155,-247.026 155,-229.538"/>
+<polygon fill="black" stroke="black" points="158.5,-229.297 155,-219.297 151.5,-229.297 158.5,-229.297"/>
+<text text-anchor="start" x="159.201" y="-250.525" font-family="Courier,monospace" font-weight="bold" font-size="12.00" fill="#3184e4">(1) </text>
+<text text-anchor="start" x="188.005" y="-250.525" font-family="Courier,monospace" font-weight="bold" font-size="12.00">Data</text>
+<text text-anchor="start" x="155" y="-240.125" font-family="Courier,monospace" font-size="10.00">Description</text>
 </g>
 </g>
 </svg>
diff --git a/examples/webapp.flow b/examples/webapp.flow
new file mode 100644
--- /dev/null
+++ b/examples/webapp.flow
@@ -0,0 +1,15 @@
+diagram 'Webapp' {
+  boundary 'Browser' {
+    function client 'Client'
+  }
+  boundary 'Amazon AWS' {
+    function server 'Web Server'
+    database logs 'Logs'
+  }
+  io analytics 'Google<br/>Analytics'
+
+  client -> server 'Request /' ''
+  server -> logs 'Log' 'User IP'
+  server -> client 'Response' 'User Profile'
+  analytics <- client 'Log' 'Page Navigation'
+}
diff --git a/examples/webapp.hs b/examples/webapp.hs
deleted file mode 100644
--- a/examples/webapp.hs
+++ /dev/null
@@ -1,24 +0,0 @@
-module Main where
-
-import DataFlow.Core
-import DataFlow.Graphviz.Renderer
-import DataFlow.DFD
-
-main :: IO ()
-main = putStr $ renderGraphviz $ asDFD  $
-  Diagram (Just "Webapp") [
-    TrustBoundary "browser" "Browser" [
-      Function "client" "Client"
-    ],
-    TrustBoundary "aws" "Amazon AWS" [
-      Function "server" "Web Server",
-      Database "logs" "Logs"
-    ],
-    InputOutput "analytics" "Google<br/>Analytics",
-
-    Flow "client" "server" "Request /" "",
-    Flow "server" "logs" "Log" "User IP",
-    Flow "server" "client" "Response" "User Profile",
-
-    Flow "client" "analytics" "Log" "Page Navigation"
-  ]
diff --git a/examples/webapp.svg b/examples/webapp.svg
--- a/examples/webapp.svg
+++ b/examples/webapp.svg
@@ -4,72 +4,72 @@
 <!-- Generated by graphviz version 2.38.0 (20140413.2041)
  -->
 <!-- Title: Webapp Pages: 1 -->
-<svg width="364pt" height="450pt"
- viewBox="0.00 0.00 364.30 450.40" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 446.4)">
+<svg width="410pt" height="470pt"
+ viewBox="0.00 0.00 409.97 470.25" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 466.246)">
 <title>Webapp</title>
-<polygon fill="white" stroke="none" points="-4,4 -4,-446.4 360.298,-446.4 360.298,4 -4,4"/>
-<text text-anchor="start" x="141.084" y="-423.4" font-family="Arial" font-weight="bold" font-size="20.00">Webapp</text>
+<polygon fill="white" stroke="none" points="-4,4 -4,-466.246 405.972,-466.246 405.972,4 -4,4"/>
+<text text-anchor="start" x="163.92" y="-443.246" font-family="Arial" font-weight="bold" font-size="20.00">Webapp</text>
 <g id="clust1" class="cluster"><title>cluster_browser</title>
-<polygon fill="none" stroke="#595959" stroke-dasharray="5,2" points="124.298,-298.185 124.298,-403.402 210.298,-403.402 210.298,-298.185 124.298,-298.185"/>
-<text text-anchor="start" x="148.961" y="-392.402" font-family="Arial" font-style="italic" font-size="10.00" fill="#595959">Browser</text>
+<polygon fill="none" stroke="#595959" stroke-dasharray="5,2" points="139.472,-8 139.472,-127.396 239.472,-127.396 239.472,-8 139.472,-8"/>
+<text text-anchor="start" x="171.134" y="-116.396" font-family="Arial" font-style="italic" font-size="10.00" fill="#595959">Browser</text>
 </g>
-<g id="clust2" class="cluster"><title>cluster_aws</title>
-<polygon fill="none" stroke="#595959" stroke-dasharray="5,2" points="75.2982,-8 75.2982,-258.185 211.298,-258.185 211.298,-8 75.2982,-8"/>
-<text text-anchor="start" x="112.639" y="-247.185" font-family="Arial" font-style="italic" font-size="10.00" fill="#595959">Amazon AWS</text>
+<g id="clust2" class="cluster"><title>cluster_amazonaws</title>
+<polygon fill="none" stroke="#595959" stroke-dasharray="5,2" points="22.4718,-167.396 22.4718,-423.248 164.472,-423.248 164.472,-167.396 22.4718,-167.396"/>
+<text text-anchor="start" x="62.8126" y="-412.248" font-family="Arial" font-style="italic" font-size="10.00" fill="#595959">Amazon AWS</text>
 </g>
 <!-- client -->
 <g id="node1" class="node"><title>client</title>
-<ellipse fill="none" stroke="black" cx="167.298" cy="-341.044" rx="34.7182" ry="34.7182"/>
-<text text-anchor="start" x="149.402" y="-338.893" font-family="Arial" font-weight="bold" font-size="14.00">Client</text>
+<ellipse fill="none" stroke="black" cx="189.472" cy="-57.9485" rx="41.897" ry="41.897"/>
+<text text-anchor="start" x="164.268" y="-54.7485" font-family="Courier,monospace" font-weight="bold" font-size="14.00">Client</text>
 </g>
 <!-- server -->
 <g id="node2" class="node"><title>server</title>
-<ellipse fill="none" stroke="black" cx="143.298" cy="-170.343" rx="60.1865" ry="60.1865"/>
-<text text-anchor="start" x="106.47" y="-168.192" font-family="Arial" font-weight="bold" font-size="14.00">Web Server</text>
-</g>
-<!-- client&#45;&gt;server -->
-<g id="edge1" class="edge"><title>client&#45;&gt;server</title>
-<path fill="none" stroke="black" d="M132.239,-339.791C94.5448,-337.407 36.2297,-327.148 7.93494,-288.185 -19.1714,-250.859 30.7957,-217.498 77.4951,-196.076"/>
-<polygon fill="black" stroke="black" points="79.0523,-199.214 86.7676,-191.953 76.2082,-192.817 79.0523,-199.214"/>
-<text text-anchor="start" x="8.29822" y="-275.484" font-family="Arial" font-weight="bold" font-size="12.00" fill="#3184e4">(1) </text>
-<text text-anchor="start" x="26.2982" y="-275.484" font-family="Arial" font-weight="bold" font-size="12.00">Request /</text>
-</g>
-<!-- client&#45;&gt;server -->
-<g id="edge3" class="edge"><title>client&#45;&gt;server</title>
-<path fill="none" stroke="black" d="M156.033,-297.404C155.38,-294.295 154.78,-291.199 154.263,-288.185 151.076,-269.61 148.761,-249.204 147.103,-230.715"/>
-<polygon fill="black" stroke="black" points="152.647,-298.299 158.261,-307.284 159.475,-296.759 152.647,-298.299"/>
-<text text-anchor="start" x="154.298" y="-279.585" font-family="Arial" font-weight="bold" font-size="12.00" fill="#3184e4">(3) </text>
-<text text-anchor="start" x="172.298" y="-279.585" font-family="Arial" font-weight="bold" font-size="12.00">Response</text>
-<text text-anchor="start" x="164.198" y="-269.185" font-family="Arial" font-size="10.00">User Profile</text>
+<ellipse fill="none" stroke="black" cx="93.4718" cy="-238.572" rx="63.353" ry="63.353"/>
+<text text-anchor="start" x="51.4649" y="-235.372" font-family="Courier,monospace" font-weight="bold" font-size="14.00">Web Server</text>
 </g>
-<!-- analytics -->
-<g id="node4" class="node"><title>analytics</title>
-<polygon fill="none" stroke="black" stroke-width="2" points="356.298,-210.343 276.298,-210.343 276.298,-130.343 356.298,-130.343 356.298,-210.343"/>
-<text text-anchor="start" x="293.726" y="-174.143" font-family="Arial" font-weight="bold" font-size="14.00">Google</text>
-<text text-anchor="start" x="288.288" y="-160.143" font-family="Arial" font-weight="bold" font-size="14.00">Analytics</text>
+<!-- server&#45;&gt;client -->
+<g id="edge1" class="edge"><title>server&#45;&gt;client</title>
+<path fill="none" stroke="black" d="M38.7491,-206.273C12.0359,-186.823 -11.0623,-160.767 5.85655,-135.396 34.9435,-91.7784 94.114,-73.0083 137.453,-64.9548"/>
+<polygon fill="black" stroke="black" points="138.286,-68.363 147.548,-63.2173 137.099,-61.4645 138.286,-68.363"/>
+<text text-anchor="start" x="6.47178" y="-143.796" font-family="Courier,monospace" font-weight="bold" font-size="12.00" fill="#3184e4">(1) </text>
+<text text-anchor="start" x="35.2765" y="-143.796" font-family="Courier,monospace" font-weight="bold" font-size="12.00">Request /</text>
 </g>
-<!-- client&#45;&gt;analytics -->
-<g id="edge4" class="edge"><title>client&#45;&gt;analytics</title>
-<path fill="none" stroke="black" d="M196.993,-322.619C211.093,-313.472 227.595,-301.419 240.298,-288.185 259.899,-267.764 277.455,-241.61 290.743,-219.226"/>
-<polygon fill="black" stroke="black" points="293.797,-220.937 295.803,-210.534 287.747,-217.415 293.797,-220.937"/>
-<text text-anchor="start" x="275.979" y="-279.585" font-family="Arial" font-weight="bold" font-size="12.00" fill="#3184e4">(4) </text>
-<text text-anchor="start" x="293.979" y="-279.585" font-family="Arial" font-weight="bold" font-size="12.00">Log</text>
-<text text-anchor="start" x="258.298" y="-269.185" font-family="Arial" font-size="10.00">Page Navigation</text>
+<!-- server&#45;&gt;client -->
+<g id="edge3" class="edge"><title>server&#45;&gt;client</title>
+<path fill="none" stroke="black" d="M127.976,-173.372C142.103,-147.086 157.928,-117.641 169.986,-95.2041"/>
+<polygon fill="black" stroke="black" points="124.709,-172.056 123.058,-182.522 130.875,-175.37 124.709,-172.056"/>
+<text text-anchor="start" x="147.472" y="-148.796" font-family="Courier,monospace" font-weight="bold" font-size="12.00" fill="#3184e4">(3) </text>
+<text text-anchor="start" x="176.276" y="-148.796" font-family="Courier,monospace" font-weight="bold" font-size="12.00">Response</text>
+<text text-anchor="start" x="154.673" y="-138.396" font-family="Courier,monospace" font-size="10.00">User Profile</text>
 </g>
 <!-- logs -->
 <g id="node3" class="node"><title>logs</title>
-<text text-anchor="start" x="140.119" y="-31.8" font-family="Arial" font-weight="bold" font-size="14.00">Logs</text>
-<polyline fill="none" stroke="black" points="135.298,-21 175.298,-21 "/>
-<polyline fill="none" stroke="black" points="175.298,-47 135.298,-47 "/>
+<text text-anchor="start" x="86.169" y="-374.548" font-family="Courier,monospace" font-weight="bold" font-size="14.00">Logs</text>
+<polyline fill="none" stroke="black" points="80.9718,-365.748 123.972,-365.748 "/>
+<polyline fill="none" stroke="black" points="123.972,-389.748 80.9718,-389.748 "/>
 </g>
-<!-- server&#45;&gt;logs -->
-<g id="edge2" class="edge"><title>server&#45;&gt;logs</title>
-<path fill="none" stroke="black" d="M148.597,-110.027C150.083,-93.3881 151.627,-76.0972 152.858,-62.3138"/>
-<polygon fill="black" stroke="black" points="156.368,-62.3631 153.772,-52.0913 149.396,-61.7403 156.368,-62.3631"/>
-<text text-anchor="start" x="152.298" y="-83.4" font-family="Arial" font-weight="bold" font-size="12.00" fill="#3184e4">(2) </text>
-<text text-anchor="start" x="170.298" y="-83.4" font-family="Arial" font-weight="bold" font-size="12.00">Log</text>
-<text text-anchor="start" x="154.639" y="-73" font-family="Arial" font-size="10.00">User IP</text>
+<!-- logs&#45;&gt;server -->
+<g id="edge2" class="edge"><title>logs&#45;&gt;server</title>
+<path fill="none" stroke="black" d="M101.35,-359.652C100.547,-347.405 99.4043,-329.994 98.2381,-312.219"/>
+<polygon fill="black" stroke="black" points="101.698,-311.497 97.5511,-301.748 94.7134,-311.956 101.698,-311.497"/>
+<text text-anchor="start" x="100.472" y="-333.148" font-family="Courier,monospace" font-weight="bold" font-size="12.00" fill="#3184e4">(2) </text>
+<text text-anchor="start" x="129.276" y="-333.148" font-family="Courier,monospace" font-weight="bold" font-size="12.00">Log</text>
+<text text-anchor="start" x="104.672" y="-322.748" font-family="Courier,monospace" font-size="10.00">User IP</text>
+</g>
+<!-- analytics -->
+<g id="node4" class="node"><title>analytics</title>
+<polygon fill="none" stroke="black" stroke-width="2" points="401.972,-288.072 302.972,-288.072 302.972,-189.072 401.972,-189.072 401.972,-288.072"/>
+<text text-anchor="start" x="327.768" y="-242.372" font-family="Courier,monospace" font-weight="bold" font-size="14.00">Google</text>
+<text text-anchor="start" x="315.166" y="-228.372" font-family="Courier,monospace" font-weight="bold" font-size="14.00">Analytics</text>
+</g>
+<!-- analytics&#45;&gt;client -->
+<g id="edge4" class="edge"><title>analytics&#45;&gt;client</title>
+<path fill="none" stroke="black" d="M307.931,-188.763C281.593,-159.9 248.722,-123.878 224.292,-97.1064"/>
+<polygon fill="black" stroke="black" points="226.645,-94.4922 217.319,-89.4647 221.474,-99.2107 226.645,-94.4922"/>
+<text text-anchor="start" x="296.275" y="-148.796" font-family="Courier,monospace" font-weight="bold" font-size="12.00" fill="#3184e4">(4) </text>
+<text text-anchor="start" x="325.08" y="-148.796" font-family="Courier,monospace" font-weight="bold" font-size="12.00">Log</text>
+<text text-anchor="start" x="276.472" y="-138.396" font-family="Courier,monospace" font-size="10.00">Page Navigation</text>
 </g>
 </g>
 </svg>
diff --git a/src/DataFlow/Core.hs b/src/DataFlow/Core.hs
--- a/src/DataFlow/Core.hs
+++ b/src/DataFlow/Core.hs
@@ -17,7 +17,7 @@
 type Description = String
 
 -- | The top level diagram.
-data Diagram = Diagram (Maybe Name) [Object]
+data Diagram = Diagram (Maybe Name) [Object] deriving (Eq, Show)
 
 -- | An object in a diagram.
 data Object =
diff --git a/src/DataFlow/Reader.hs b/src/DataFlow/Reader.hs
new file mode 100644
--- /dev/null
+++ b/src/DataFlow/Reader.hs
@@ -0,0 +1,131 @@
+module DataFlow.Reader (readDiagram, readDiagramFile) where
+
+import Text.ParserCombinators.Parsec
+import Data.Char
+import DataFlow.Core
+
+nameToID :: String -> String
+nameToID = filter isLetter . map toLower
+
+identifier :: Parser ID
+identifier = do
+  first <- letter
+  rest <-  many (letter <|> digit <|> char '_')
+  return $ first : rest
+
+quoted :: Parser ID
+quoted = do
+  -- TODO: Handle escaped characters.
+  _ <- char '\''
+  s <- many (noneOf "'")
+  _ <- char '\''
+  return s
+
+skipWhitespace :: Parser ()
+skipWhitespace = skipMany $ space <|> newline
+
+skipWhitespace1 :: Parser ()
+skipWhitespace1 = skipMany $ space <|> newline
+
+inBraces :: Parser t -> Parser t
+inBraces inside = do
+  skipWhitespace
+  _ <- char '{'
+  skipWhitespace
+  c <- inside
+  skipWhitespace
+  _ <- char '}'
+  skipWhitespace
+  return c
+
+idAndNameObject :: String -> (ID -> ID -> t) -> Parser t
+idAndNameObject keyword f = do
+  _ <- string keyword
+  skipMany1 space
+  id' <- identifier
+  skipMany1 space
+  name <- quoted
+  skipWhitespace1
+  return $ f id' name
+
+function :: Parser Object
+function = idAndNameObject "function" Function
+
+database :: Parser Object
+database = idAndNameObject "database" Database
+
+io :: Parser Object
+io = idAndNameObject "io" InputOutput
+
+data FlowType = Back | Forward
+
+arrow :: Parser FlowType
+arrow = do
+  s <- string "->" <|> string "--" <|> string "<-"
+  case s of
+    "->" -> return Forward
+    "<-" -> return Back
+    _ -> fail "Invalid flow statement"
+
+flow :: Parser Object
+flow = do
+  i1 <- identifier
+  skipMany1 space
+  a <- arrow
+  skipMany1 space
+  i2 <- identifier
+  skipMany1 space
+  data' <- quoted
+  skipMany1 space
+  desc <- quoted
+  skipWhitespace1
+  case a of
+    Back -> return $ Flow i2 i1 data' desc
+    Forward -> return $ Flow i1 i2 data' desc
+
+boundary :: Parser Object
+boundary = do
+  _ <- string "boundary"
+  skipMany1 space
+  name <- quoted
+  let id' = nameToID name
+  skipMany1 space
+  objs <- inBraces objects
+  skipWhitespace1
+  return $ TrustBoundary id' name objs
+
+object :: Parser Object
+object =
+  try boundary
+  <|> try function
+  <|> try database
+  <|> try io
+  <|> flow
+
+objects :: Parser [Object]
+objects = object `sepBy` many (space <|> newline)
+
+namedDiagram :: Parser Diagram
+namedDiagram = do
+  _ <- string "diagram"
+  skipMany1 space
+  name <- quoted
+  skipMany1 space
+  objs <- inBraces objects
+  return $ Diagram (Just name) objs
+
+unnamedDiagram :: Parser Diagram
+unnamedDiagram = do
+  _ <- string "diagram"
+  skipMany1 space
+  objs <- inBraces objects
+  return $ Diagram Nothing objs
+
+diagram :: Parser Diagram
+diagram = try unnamedDiagram <|> namedDiagram
+
+readDiagram :: String -> String ->  Either ParseError Diagram
+readDiagram = parse diagram
+
+readDiagramFile :: FilePath -> IO (Either ParseError Diagram)
+readDiagramFile = parseFromFile diagram
