It is somewhat essential when creating a SaaS product to have a great demo.
For the longest time I was manually managing a demo organisation in AgendaScope. It really wasn’t a very good demo — the items weren’t well thought out and it had gotten ‘crufty’ - and, worse, I kept having to manually “reset” it, which was becoming increasingly painful.
The straw that broke the camels back was me spending hours painstakingly building a new demo and then accidentally dropping the database it was in, with no backup. No, no, a thousand times no!
Alright… how cool would it be if I could whistle up a great demo, fresh every time I needed it? What would that take?
…some time later…
The answer is DemoGen: an elixir library for building demos from a “script” akin to a movie script. Here are some excerpts code from the AgendaScope “TechHarbour” demo script:
# Setup TechHarbour Ltd & its Leadership team
$subdomain = "tharbour"
delete_org {subdomain: $subdomain}
# Same password for all users
$password = "foo-bar-baz-qux"
# Some macros as short-hand for adjusting the clock
macro last_month = alter_clock {M: -1}
macro next_day = alter_clock {D: +1}
# Off we go, demo starts 1 month ago
@last_month
[09:00] add_org {as: org name: "TechHarbour" subdomain: $subdomain}
set_feature_flag {org: org flag: "show_ai_assistant"}
set_feature_flag {org: org flag: "show_score_heatmaps"}
add_group {as: leadership org: org name: "Leadership"}
set_objectives {
group: leadership
o_1: "MRR £1.2m (£0.8m)"
o_2: "Top-3 Customers <45% (67%)"
o_3: "Avg. Debtor Days 31 (75)"
}
# Setup Emily Walker, CEO
[09:05] add_account {as: emily name: "Emily Walker" email: "e.walker@techharbour.com" password: $password}
join_org {user: emily org: org admin: false}
join_group {as: m_1 user: emily group: leadership role: "CEO" admin: true}
@next_day
# Add our first opportunity
[10:00] add_item {
as: i_ai_research group: leadership creator: emily
item_type: "opportunity" title: "Automating expensive research processes" public: true
detail: "At the moment the core business is constrained by researchers doing a lot of manual research. Could we use AI to speed up the research process?"
}
tag_item {item: i_ai_research tag: "innovation"}
set_tag_color {group: leadership tag: "innovation" color: "indigo"}
tag_item {item: i_ai_research tag: "vision"}
set_tag_color {group: leadership tag: "vision" color: "red"}
[10:01] set_champion {item: i_ai_research champion: emily}
[10:05] score_item {item: i_ai_research user: emily impact: 80 timeframe: 76 likelihood: 75}
[10:15] add_comment {
as: c_ai_research_1 item: i_ai_research user: emily
body: "Has anyone here seen an up-to-date review of OpenAI vs Anthropic?"
}
@next_day
[10:15] score_item {item: i_ai_research user: sophie impact: 92 timeframe: 90 likelihood: 90}
[10:20] score_item {item: i_ai_research user: isabella impact: 45 timeframe: 45 likelihood: 45}
[10:21] score_item {item: i_ai_research user: oliver impact: 79 timeframe: 81 likelihood: 68}
[11:30] score_item {item: i_ai_research user: james impact: 95 timeframe: 98 likelihood: 88}
[11:45] add_comment {
as: c_ai_research_2 item: i_ai_research user: isabella
body: "I've no idea what any of this stuff means."
}
[14:30] add_comment {
as: c_ai_research_3 item: i_ai_research user: sophie
body: "We have been experimenting with the APIs, GPT 4-o is better for 'reasoning' tasks while Claude seems to generate a better output."
}
...
The syntax for the demo script uses #
to start a line comment and $
to create a global value:
$variable = value
Global values can be referenced from within command arguments. Commands are the things that make changes.
Next we have macros that are a convenient shortcut for commonly used commands.
macro next_day = alter_clock {D: +1}
can be invoked via:
@next_day
Then we have time codes like:
[09:00]
which are a sugar for alter_clock {h: _, m: _}
. The current clock date/time is passed as t
to every command.
Lastly we have the demo-driven commands themselves add_org
, set_feature_flag
, add_group
, and so on. These do the heavy lifting of creating/modifying application models.
Each command is implemented by a module that implements a particular Command
behaviour.
Here is our add_org
for example:
defmodule Radar.Demo.Commands.AddOrg do
@impl Radar.Demo.Commands.Command
def run(args, %{time: t, symbols: symbols} = context) do
{:string, name} = Map.get(args, :name)
{:string, subdomain} = Map.get(args, :subdomain)
{:symbol, as} = Map.get(args, :as)
with {:ok, %Org{} = org} <- create_org(t, name, subdomain) do
{:ok, %{context | symbols: Map.put(symbols, as, org)}}
end
end
end
The run
function is always called with a map of arguments and a context.
In the example the call to the add_org
command there are three arguments as
, name
, and subdomain
.
The context consists of the “current” datetime t
and symbols
which is a map of objects created by the demo already. This is actually one of the cool bits because it removes the need for database lookups in subsequent commands.
Looking at the previous example the add_org
commands specifies as: org
so that the newly created %Org{}
model is stored under key the org
in the symbols table.
Later when the add_group
command specifies org: org
that %Org{}
model is retrieved from the symbols table and can be used directly.
In this way we build up our demo model-by-model. It’s really kind of neat.
When the demo script is parsed it automaticallly converts a command like add_org
into a module name (in my case Radar.Demo.Commands.AddOrgCommand
) and expects it to implement run/2
to implement its behaviour and stash any models it creates into the symbols
table. So you implement a module for each command and away you go.
Whenever you add new functionality to your app you can create a new command module and add it to your demo script. That’s it.
Lastly DemoGen
provides an Oban worker implementation that can run demo scripts. In AgendaScope we have a simple upload form that triggers the worker. In this way I can quickly reset the demo before a call.
At the moment the code is part of the AgendaScope project but I am intending to extract it into a separate library both for my own use and potentiall others if this is interesting.