Skip to contentSkip to navigationSkip to topbar
On this page

Workflow Automation with Python and Flask


One of the more abstract concepts you'll handle when building your business is what the workflow will look like.

At its core, setting up a standardized workflow is about enabling your service providers (agents, hosts, customer service reps, administrators, and the rest of the gang) to better serve your customers.

To illustrate a very real-world example, today we'll build a Python and Flask webapp for finding and booking vacation properties — tentatively called Airtng.

Here's how it'll work:

  1. A host creates a vacation property listing
  2. A guest requests a reservation for a property
  3. The host receives an SMS notifying them of the reservation request. The host can either Accept or Reject the reservation
  4. The guest is notified whether a request was rejected or accepted

Learn how Airbnb used Twilio SMS to streamline the rental experience for 60M+ travelers around the world.(link takes you to an external page)


Workflow Building Blocks

workflow-building-blocks page anchor

We'll be using the Twilio REST API to send our users messages at important junctures. Here's a bit more on our API:

  • Sending Messages with Twilio API

Load the application configuration

load-the-application-configuration page anchor

airtng_flask/__init__.py

1
import os
2
from airtng_flask.config import config_env_files
3
from flask import Flask
4
5
from flask_bcrypt import Bcrypt
6
from flask_sqlalchemy import SQLAlchemy
7
from flask_login import LoginManager
8
9
db = SQLAlchemy()
10
bcrypt = Bcrypt()
11
login_manager = LoginManager()
12
13
14
def create_app(config_name='development', p_db=db, p_bcrypt=bcrypt, p_login_manager=login_manager):
15
new_app = Flask(__name__)
16
config_app(config_name, new_app)
17
new_app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
18
19
p_db.init_app(new_app)
20
p_bcrypt.init_app(new_app)
21
p_login_manager.init_app(new_app)
22
p_login_manager.login_view = 'register'
23
return new_app
24
25
26
def config_app(config_name, new_app):
27
new_app.config.from_object(config_env_files[config_name])
28
29
30
app = create_app()
31
32
import airtng_flask.views

Let's get started! Click the below button to begin.


User and Session Management

user-and-session-management page anchor

For this workflow to work, we need to have Users created in our application, and allow them to log into Airtng.

Our User model stores a user's basic information including their phone number. We'll use that to send them SMS notifications later.

airtng_flask/models/user.py

1
from airtng_flask.models import app_db
2
from airtng_flask.models import bcrypt
3
4
db = app_db()
5
bcrypt = bcrypt()
6
7
8
class User(db.Model):
9
__tablename__ = "users"
10
11
id = db.Column(db.Integer, primary_key=True)
12
name = db.Column(db.String, nullable=False)
13
email = db.Column(db.String, nullable=False)
14
password = db.Column(db.String, nullable=False)
15
phone_number = db.Column(db.String, nullable=False)
16
17
reservations = db.relationship("Reservation", back_populates="guest")
18
vacation_properties = db.relationship("VacationProperty", back_populates="host")
19
20
def __init__(self, name, email, password, phone_number):
21
self.name = name
22
self.email = email
23
self.password = bcrypt.generate_password_hash(password)
24
self.phone_number = phone_number
25
26
def is_authenticated(self):
27
return True
28
29
def is_active(self):
30
return True
31
32
def is_anonymous(self):
33
return False
34
35
def get_id(self):
36
try:
37
return unicode(self.id)
38
except NameError:
39
return str(self.id)
40
41
# Python 3
42
43
def __unicode__(self):
44
return self.name
45
46
def __repr__(self):
47
return '<User %r>' % (self.name)

Next, let's look at how we define the VacationProperty model.


The VacationProperty Model

the-vacationproperty-model page anchor

In order to build a vacation rentals company we need a way to create the property listings.

The VacationProperty model belongs to the User who created it (we'll call this user the host moving forward) and contains only two properties: a description and an image_url.

We also include a couple database relationship fields to help us link vacation properties to their hosts as well as to any reservations our users make.

airtng_flask/models/vacation_property.py

1
from airtng_flask.models import app_db
2
3
db = app_db()
4
5
6
class VacationProperty(db.Model):
7
__tablename__ = "vacation_properties"
8
9
id = db.Column(db.Integer, primary_key=True)
10
description = db.Column(db.String, nullable=False)
11
image_url = db.Column(db.String, nullable=False)
12
13
host_id = db.Column(db.Integer, db.ForeignKey('users.id'))
14
host = db.relationship("User", back_populates="vacation_properties")
15
reservations = db.relationship("Reservation", back_populates="vacation_property")
16
17
def __init__(self, description, image_url, host):
18
self.description = description
19
self.image_url = image_url
20
self.host = host
21
22
def __repr__(self):
23
return '<VacationProperty %r %r>' % self.id, self.description

Next we'll take a look at how to model a reservation.


The Reservation model is at the center of the workflow for this application. It is responsible for keeping track of:

  • The guest who performed the reservation
  • The vacation_property the guest is requesting (and associated host)
  • The status of the reservation: pending, confirmed, or rejected

airtng_flask/models/reservation.py

1
from airtng_flask.models import app_db, auth_token, account_sid, phone_number
2
from flask import render_template
3
from twilio.rest import Client
4
5
db = app_db()
6
7
8
class Reservation(db.Model):
9
__tablename__ = "reservations"
10
11
id = db.Column(db.Integer, primary_key=True)
12
message = db.Column(db.String, nullable=False)
13
status = db.Column(db.Enum('pending', 'confirmed', 'rejected', name='reservation_status_enum'), default='pending')
14
15
guest_id = db.Column(db.Integer, db.ForeignKey('users.id'))
16
vacation_property_id = db.Column(db.Integer, db.ForeignKey('vacation_properties.id'))
17
guest = db.relationship("User", back_populates="reservations")
18
vacation_property = db.relationship("VacationProperty", back_populates="reservations")
19
20
def __init__(self, message, vacation_property, guest):
21
self.message = message
22
self.guest = guest
23
self.vacation_property = vacation_property
24
self.status = 'pending'
25
26
def confirm(self):
27
self.status = 'confirmed'
28
29
def reject(self):
30
self.status = 'rejected'
31
32
def __repr__(self):
33
return '<VacationProperty %r %r>' % self.id, self.name
34
35
def notify_host(self):
36
self._send_message(self.vacation_property.host.phone_number,
37
render_template('messages/sms_host.txt',
38
name=self.guest.name,
39
description=self.vacation_property.description,
40
message=self.message))
41
42
def notify_guest(self):
43
self._send_message(self.guest.phone_number,
44
render_template('messages/sms_guest.txt',
45
description=self.vacation_property.description,
46
status=self.status))
47
48
def _get_twilio_client(self):
49
return Client(account_sid(), auth_token())
50
51
def _send_message(self, to, message):
52
self._get_twilio_client().messages.create(
53
to=to,
54
from_=phone_number(),
55
body=message)

Now that we have our models, let's see how a user would create a reservation.


The reservation creation form holds only one field, the message that will be sent to the host user when reserving one of her properties.

The rest of the information necessary to create a reservation is taken from the user that is logged into the system and the relationship between a property and its owning host.

A reservation is created with a default status pending, so when the host replies with an accept or reject response, the system knows which reservation to update.

Routes for the Airtng workflow

routes-for-the-airtng-workflow page anchor

airtng_flask/views.py

1
from airtng_flask import db, bcrypt, app, login_manager
2
from flask import session, g, request, flash, Blueprint
3
from flask_login import login_user, logout_user, current_user, login_required
4
from twilio.twiml.voice_response import VoiceResponse
5
6
from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \
7
ReservationConfirmationForm
8
from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params
9
from airtng_flask.models import init_models_module
10
11
init_models_module(db, bcrypt, app)
12
13
from airtng_flask.models.user import User
14
from airtng_flask.models.vacation_property import VacationProperty
15
from airtng_flask.models.reservation import Reservation
16
17
18
@app.route('/', methods=["GET", "POST"])
19
@app.route('/register', methods=["GET", "POST"])
20
def register():
21
form = RegisterForm()
22
if request.method == 'POST':
23
if form.validate_on_submit():
24
25
if User.query.filter(User.email == form.email.data).count() > 0:
26
form.email.errors.append("Email address already in use.")
27
return view('register', form)
28
29
user = User(
30
name=form.name.data,
31
email=form.email.data,
32
password=form.password.data,
33
phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data)
34
)
35
db.session.add(user)
36
db.session.commit()
37
login_user(user, remember=True)
38
39
return redirect_to('home')
40
else:
41
return view('register', form)
42
43
return view('register', form)
44
45
46
@app.route('/login', methods=["GET", "POST"])
47
def login():
48
form = LoginForm()
49
if request.method == 'POST':
50
if form.validate_on_submit():
51
candidate_user = User.query.filter(User.email == form.email.data).first()
52
53
if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,
54
form.password.data):
55
form.password.errors.append("Invalid credentials.")
56
return view('login', form)
57
58
login_user(candidate_user, remember=True)
59
return redirect_to('home')
60
return view('login', form)
61
62
63
@app.route('/logout', methods=["POST"])
64
@login_required
65
def logout():
66
logout_user()
67
return redirect_to('home')
68
69
70
@app.route('/home', methods=["GET"])
71
@login_required
72
def home():
73
return view('home')
74
75
76
@app.route('/properties', methods=["GET"])
77
@login_required
78
def properties():
79
vacation_properties = VacationProperty.query.all()
80
return view_with_params('properties', vacation_properties=vacation_properties)
81
82
83
@app.route('/properties/new', methods=["GET", "POST"])
84
@login_required
85
def new_property():
86
form = VacationPropertyForm()
87
if request.method == 'POST':
88
if form.validate_on_submit():
89
host = User.query.get(current_user.get_id())
90
91
property = VacationProperty(form.description.data, form.image_url.data, host)
92
db.session.add(property)
93
db.session.commit()
94
return redirect_to('properties')
95
96
return view('property_new', form)
97
98
99
@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})
100
@app.route('/reservations/<property_id>', methods=["GET", "POST"])
101
@login_required
102
def new_reservation(property_id):
103
vacation_property = None
104
form = ReservationForm()
105
form.property_id.data = property_id
106
107
if request.method == 'POST':
108
if form.validate_on_submit():
109
guest = User.query.get(current_user.get_id())
110
111
vacation_property = VacationProperty.query.get(form.property_id.data)
112
reservation = Reservation(form.message.data, vacation_property, guest)
113
db.session.add(reservation)
114
db.session.commit()
115
116
reservation.notify_host()
117
118
return redirect_to('properties')
119
120
if property_id is not None:
121
vacation_property = VacationProperty.query.get(property_id)
122
123
return view_with_params('reservation', vacation_property=vacation_property, form=form)
124
125
126
@app.route('/confirm', methods=["POST"])
127
def confirm_reservation():
128
form = ReservationConfirmationForm()
129
sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."
130
131
user = User.query.filter(User.phone_number == form.From.data).first()
132
reservation = Reservation \
133
.query \
134
.filter(Reservation.status == 'pending'
135
and Reservation.vacation_property.host.id == user.id) \
136
.first()
137
138
if reservation is not None:
139
140
if 'yes' in form.Body.data or 'accept' in form.Body.data:
141
reservation.confirm()
142
else:
143
reservation.reject()
144
145
db.session.commit()
146
147
sms_response_text = "You have successfully {0} the reservation".format(reservation.status)
148
reservation.notify_guest()
149
150
return twiml(_respond_message(sms_response_text))
151
152
153
# controller utils
154
@app.before_request
155
def before_request():
156
g.user = current_user
157
uri_pattern = request.url_rule
158
if current_user.is_authenticated and (
159
uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):
160
redirect_to('home')
161
162
163
@login_manager.user_loader
164
def load_user(id):
165
try:
166
return User.query.get(id)
167
except:
168
return None
169
170
171
def _respond_message(message):
172
response = VoiceResponse()
173
response.message(message)
174
return response

Now that we have seen how we will initiate a reservation, let's look at how to notify the host.


When a reservation is created for a property, we want to notify the host of the reservation request.

We use Twilio's Rest API to send a SMS message to the host, using a Twilio phone number(link takes you to an external page).

Now we just have to wait for the host to send an SMS response accepting or rejecting the reservation. At that point we can notify the user and host and update the reservation information accordingly.

Notify the user and the host

notify-the-user-and-the-host page anchor

airtng_flask/models/reservation.py

1
from airtng_flask.models import app_db, auth_token, account_sid, phone_number
2
from flask import render_template
3
from twilio.rest import Client
4
5
db = app_db()
6
7
8
class Reservation(db.Model):
9
__tablename__ = "reservations"
10
11
id = db.Column(db.Integer, primary_key=True)
12
message = db.Column(db.String, nullable=False)
13
status = db.Column(db.Enum('pending', 'confirmed', 'rejected', name='reservation_status_enum'), default='pending')
14
15
guest_id = db.Column(db.Integer, db.ForeignKey('users.id'))
16
vacation_property_id = db.Column(db.Integer, db.ForeignKey('vacation_properties.id'))
17
guest = db.relationship("User", back_populates="reservations")
18
vacation_property = db.relationship("VacationProperty", back_populates="reservations")
19
20
def __init__(self, message, vacation_property, guest):
21
self.message = message
22
self.guest = guest
23
self.vacation_property = vacation_property
24
self.status = 'pending'
25
26
def confirm(self):
27
self.status = 'confirmed'
28
29
def reject(self):
30
self.status = 'rejected'
31
32
def __repr__(self):
33
return '<VacationProperty %r %r>' % self.id, self.name
34
35
def notify_host(self):
36
self._send_message(self.vacation_property.host.phone_number,
37
render_template('messages/sms_host.txt',
38
name=self.guest.name,
39
description=self.vacation_property.description,
40
message=self.message))
41
42
def notify_guest(self):
43
self._send_message(self.guest.phone_number,
44
render_template('messages/sms_guest.txt',
45
description=self.vacation_property.description,
46
status=self.status))
47
48
def _get_twilio_client(self):
49
return Client(account_sid(), auth_token())
50
51
def _send_message(self, to, message):
52
self._get_twilio_client().messages.create(
53
to=to,
54
from_=phone_number(),
55
body=message)

Next, let's see how to handle incoming messages from Twilio webhooks.


Handle Incoming Twilio Requests

handle-incoming-twilio-requests page anchor

This method handles the Twilio request triggered by the host's SMS and does three things:

  1. Checks for a pending reservation from a user
  2. Updates the status of the reservation
  3. Responds to the host and sends a notification to the user

Setting Up Incoming Twilio Requests

setting-up-incoming-twilio-requests page anchor

In the Twilio console(link takes you to an external page), you should change the 'A Message Comes In' webhook to call your application's endpoint in the route /confirm:

SMS Webhook.

One way to expose your machine to the world during development is using ngrok(link takes you to an external page). Your URL for the SMS web hook on your phone number should look something like this:

1
http://<subdomain>.ngrok.io/confirm
2

An incoming request from Twilio comes with some helpful parameters. These include the From phone number and the message Body.

We'll use the From parameter to look up the host and check if they have any pending reservations. If they do, we'll use the message body to check for the message 'accepted' or 'rejected'. Finally, we update the reservation status and use the SmsNotifier abstraction to send an SMS to the guest telling them the host accepted or rejected their reservation request.

In our response to Twilio, we'll use Twilio's TwiML Markup Language to command Twilio to send an SMS notification message to the host.

Confirm or reject a pending reservation request

confirm-or-reject-a-pending-reservation-request page anchor

airtng_flask/views.py

1
from airtng_flask import db, bcrypt, app, login_manager
2
from flask import session, g, request, flash, Blueprint
3
from flask_login import login_user, logout_user, current_user, login_required
4
from twilio.twiml.voice_response import VoiceResponse
5
6
from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \
7
ReservationConfirmationForm
8
from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params
9
from airtng_flask.models import init_models_module
10
11
init_models_module(db, bcrypt, app)
12
13
from airtng_flask.models.user import User
14
from airtng_flask.models.vacation_property import VacationProperty
15
from airtng_flask.models.reservation import Reservation
16
17
18
@app.route('/', methods=["GET", "POST"])
19
@app.route('/register', methods=["GET", "POST"])
20
def register():
21
form = RegisterForm()
22
if request.method == 'POST':
23
if form.validate_on_submit():
24
25
if User.query.filter(User.email == form.email.data).count() > 0:
26
form.email.errors.append("Email address already in use.")
27
return view('register', form)
28
29
user = User(
30
name=form.name.data,
31
email=form.email.data,
32
password=form.password.data,
33
phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data)
34
)
35
db.session.add(user)
36
db.session.commit()
37
login_user(user, remember=True)
38
39
return redirect_to('home')
40
else:
41
return view('register', form)
42
43
return view('register', form)
44
45
46
@app.route('/login', methods=["GET", "POST"])
47
def login():
48
form = LoginForm()
49
if request.method == 'POST':
50
if form.validate_on_submit():
51
candidate_user = User.query.filter(User.email == form.email.data).first()
52
53
if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,
54
form.password.data):
55
form.password.errors.append("Invalid credentials.")
56
return view('login', form)
57
58
login_user(candidate_user, remember=True)
59
return redirect_to('home')
60
return view('login', form)
61
62
63
@app.route('/logout', methods=["POST"])
64
@login_required
65
def logout():
66
logout_user()
67
return redirect_to('home')
68
69
70
@app.route('/home', methods=["GET"])
71
@login_required
72
def home():
73
return view('home')
74
75
76
@app.route('/properties', methods=["GET"])
77
@login_required
78
def properties():
79
vacation_properties = VacationProperty.query.all()
80
return view_with_params('properties', vacation_properties=vacation_properties)
81
82
83
@app.route('/properties/new', methods=["GET", "POST"])
84
@login_required
85
def new_property():
86
form = VacationPropertyForm()
87
if request.method == 'POST':
88
if form.validate_on_submit():
89
host = User.query.get(current_user.get_id())
90
91
property = VacationProperty(form.description.data, form.image_url.data, host)
92
db.session.add(property)
93
db.session.commit()
94
return redirect_to('properties')
95
96
return view('property_new', form)
97
98
99
@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})
100
@app.route('/reservations/<property_id>', methods=["GET", "POST"])
101
@login_required
102
def new_reservation(property_id):
103
vacation_property = None
104
form = ReservationForm()
105
form.property_id.data = property_id
106
107
if request.method == 'POST':
108
if form.validate_on_submit():
109
guest = User.query.get(current_user.get_id())
110
111
vacation_property = VacationProperty.query.get(form.property_id.data)
112
reservation = Reservation(form.message.data, vacation_property, guest)
113
db.session.add(reservation)
114
db.session.commit()
115
116
reservation.notify_host()
117
118
return redirect_to('properties')
119
120
if property_id is not None:
121
vacation_property = VacationProperty.query.get(property_id)
122
123
return view_with_params('reservation', vacation_property=vacation_property, form=form)
124
125
126
@app.route('/confirm', methods=["POST"])
127
def confirm_reservation():
128
form = ReservationConfirmationForm()
129
sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."
130
131
user = User.query.filter(User.phone_number == form.From.data).first()
132
reservation = Reservation \
133
.query \
134
.filter(Reservation.status == 'pending'
135
and Reservation.vacation_property.host.id == user.id) \
136
.first()
137
138
if reservation is not None:
139
140
if 'yes' in form.Body.data or 'accept' in form.Body.data:
141
reservation.confirm()
142
else:
143
reservation.reject()
144
145
db.session.commit()
146
147
sms_response_text = "You have successfully {0} the reservation".format(reservation.status)
148
reservation.notify_guest()
149
150
return twiml(_respond_message(sms_response_text))
151
152
153
# controller utils
154
@app.before_request
155
def before_request():
156
g.user = current_user
157
uri_pattern = request.url_rule
158
if current_user.is_authenticated and (
159
uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):
160
redirect_to('home')
161
162
163
@login_manager.user_loader
164
def load_user(id):
165
try:
166
return User.query.get(id)
167
except:
168
return None
169
170
171
def _respond_message(message):
172
response = VoiceResponse()
173
response.message(message)
174
return response

Congratulations!

You've just learned how to automate your workflow with Twilio Programmable SMS. In the next pane, we'll look at some other features you can add.


To improve upon this you could add anonymous communications so that the host and guest could communicate through a shared Twilio phone number: Call Masking with Python and Flask.

You might also enjoy these other tutorials:

IVR Phone Tree

Create a seamless customer service experience by building an IVR (Interactive Voice Response) Phone Tree for your company.

Click To Call

Convert web traffic into phone calls with the click of a button.

Need some help?

Terms of service

Copyright © 2024 Twilio Inc.