Skip to contentSkip to navigationSkip to topbar
Rate this page:
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


_32
import os
_32
from airtng_flask.config import config_env_files
_32
from flask import Flask
_32
_32
from flask_bcrypt import Bcrypt
_32
from flask_sqlalchemy import SQLAlchemy
_32
from flask_login import LoginManager
_32
_32
db = SQLAlchemy()
_32
bcrypt = Bcrypt()
_32
login_manager = LoginManager()
_32
_32
_32
def create_app(config_name='development', p_db=db, p_bcrypt=bcrypt, p_login_manager=login_manager):
_32
new_app = Flask(__name__)
_32
config_app(config_name, new_app)
_32
new_app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
_32
_32
p_db.init_app(new_app)
_32
p_bcrypt.init_app(new_app)
_32
p_login_manager.init_app(new_app)
_32
p_login_manager.login_view = 'register'
_32
return new_app
_32
_32
_32
def config_app(config_name, new_app):
_32
new_app.config.from_object(config_env_files[config_name])
_32
_32
_32
app = create_app()
_32
_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


_47
from airtng_flask.models import app_db
_47
from airtng_flask.models import bcrypt
_47
_47
db = app_db()
_47
bcrypt = bcrypt()
_47
_47
_47
class User(db.Model):
_47
__tablename__ = "users"
_47
_47
id = db.Column(db.Integer, primary_key=True)
_47
name = db.Column(db.String, nullable=False)
_47
email = db.Column(db.String, nullable=False)
_47
password = db.Column(db.String, nullable=False)
_47
phone_number = db.Column(db.String, nullable=False)
_47
_47
reservations = db.relationship("Reservation", back_populates="guest")
_47
vacation_properties = db.relationship("VacationProperty", back_populates="host")
_47
_47
def __init__(self, name, email, password, phone_number):
_47
self.name = name
_47
self.email = email
_47
self.password = bcrypt.generate_password_hash(password)
_47
self.phone_number = phone_number
_47
_47
def is_authenticated(self):
_47
return True
_47
_47
def is_active(self):
_47
return True
_47
_47
def is_anonymous(self):
_47
return False
_47
_47
def get_id(self):
_47
try:
_47
return unicode(self.id)
_47
except NameError:
_47
return str(self.id)
_47
_47
# Python 3
_47
_47
def __unicode__(self):
_47
return self.name
_47
_47
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


_23
from airtng_flask.models import app_db
_23
_23
db = app_db()
_23
_23
_23
class VacationProperty(db.Model):
_23
__tablename__ = "vacation_properties"
_23
_23
id = db.Column(db.Integer, primary_key=True)
_23
description = db.Column(db.String, nullable=False)
_23
image_url = db.Column(db.String, nullable=False)
_23
_23
host_id = db.Column(db.Integer, db.ForeignKey('users.id'))
_23
host = db.relationship("User", back_populates="vacation_properties")
_23
reservations = db.relationship("Reservation", back_populates="vacation_property")
_23
_23
def __init__(self, description, image_url, host):
_23
self.description = description
_23
self.image_url = image_url
_23
self.host = host
_23
_23
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


_55
from airtng_flask.models import app_db, auth_token, account_sid, phone_number
_55
from flask import render_template
_55
from twilio.rest import Client
_55
_55
db = app_db()
_55
_55
_55
class Reservation(db.Model):
_55
__tablename__ = "reservations"
_55
_55
id = db.Column(db.Integer, primary_key=True)
_55
message = db.Column(db.String, nullable=False)
_55
status = db.Column(db.Enum('pending', 'confirmed', 'rejected', name='reservation_status_enum'), default='pending')
_55
_55
guest_id = db.Column(db.Integer, db.ForeignKey('users.id'))
_55
vacation_property_id = db.Column(db.Integer, db.ForeignKey('vacation_properties.id'))
_55
guest = db.relationship("User", back_populates="reservations")
_55
vacation_property = db.relationship("VacationProperty", back_populates="reservations")
_55
_55
def __init__(self, message, vacation_property, guest):
_55
self.message = message
_55
self.guest = guest
_55
self.vacation_property = vacation_property
_55
self.status = 'pending'
_55
_55
def confirm(self):
_55
self.status = 'confirmed'
_55
_55
def reject(self):
_55
self.status = 'rejected'
_55
_55
def __repr__(self):
_55
return '<VacationProperty %r %r>' % self.id, self.name
_55
_55
def notify_host(self):
_55
self._send_message(self.vacation_property.host.phone_number,
_55
render_template('messages/sms_host.txt',
_55
name=self.guest.name,
_55
description=self.vacation_property.description,
_55
message=self.message))
_55
_55
def notify_guest(self):
_55
self._send_message(self.guest.phone_number,
_55
render_template('messages/sms_guest.txt',
_55
description=self.vacation_property.description,
_55
status=self.status))
_55
_55
def _get_twilio_client(self):
_55
return Client(account_sid(), auth_token())
_55
_55
def _send_message(self, to, message):
_55
self._get_twilio_client().messages.create(
_55
to=to,
_55
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


_174
from airtng_flask import db, bcrypt, app, login_manager
_174
from flask import session, g, request, flash, Blueprint
_174
from flask_login import login_user, logout_user, current_user, login_required
_174
from twilio.twiml.voice_response import VoiceResponse
_174
_174
from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \
_174
ReservationConfirmationForm
_174
from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params
_174
from airtng_flask.models import init_models_module
_174
_174
init_models_module(db, bcrypt, app)
_174
_174
from airtng_flask.models.user import User
_174
from airtng_flask.models.vacation_property import VacationProperty
_174
from airtng_flask.models.reservation import Reservation
_174
_174
_174
@app.route('/', methods=["GET", "POST"])
_174
@app.route('/register', methods=["GET", "POST"])
_174
def register():
_174
form = RegisterForm()
_174
if request.method == 'POST':
_174
if form.validate_on_submit():
_174
_174
if User.query.filter(User.email == form.email.data).count() > 0:
_174
form.email.errors.append("Email address already in use.")
_174
return view('register', form)
_174
_174
user = User(
_174
name=form.name.data,
_174
email=form.email.data,
_174
password=form.password.data,
_174
phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data)
_174
)
_174
db.session.add(user)
_174
db.session.commit()
_174
login_user(user, remember=True)
_174
_174
return redirect_to('home')
_174
else:
_174
return view('register', form)
_174
_174
return view('register', form)
_174
_174
_174
@app.route('/login', methods=["GET", "POST"])
_174
def login():
_174
form = LoginForm()
_174
if request.method == 'POST':
_174
if form.validate_on_submit():
_174
candidate_user = User.query.filter(User.email == form.email.data).first()
_174
_174
if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,
_174
form.password.data):
_174
form.password.errors.append("Invalid credentials.")
_174
return view('login', form)
_174
_174
login_user(candidate_user, remember=True)
_174
return redirect_to('home')
_174
return view('login', form)
_174
_174
_174
@app.route('/logout', methods=["POST"])
_174
@login_required
_174
def logout():
_174
logout_user()
_174
return redirect_to('home')
_174
_174
_174
@app.route('/home', methods=["GET"])
_174
@login_required
_174
def home():
_174
return view('home')
_174
_174
_174
@app.route('/properties', methods=["GET"])
_174
@login_required
_174
def properties():
_174
vacation_properties = VacationProperty.query.all()
_174
return view_with_params('properties', vacation_properties=vacation_properties)
_174
_174
_174
@app.route('/properties/new', methods=["GET", "POST"])
_174
@login_required
_174
def new_property():
_174
form = VacationPropertyForm()
_174
if request.method == 'POST':
_174
if form.validate_on_submit():
_174
host = User.query.get(current_user.get_id())
_174
_174
property = VacationProperty(form.description.data, form.image_url.data, host)
_174
db.session.add(property)
_174
db.session.commit()
_174
return redirect_to('properties')
_174
_174
return view('property_new', form)
_174
_174
_174
@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})
_174
@app.route('/reservations/<property_id>', methods=["GET", "POST"])
_174
@login_required
_174
def new_reservation(property_id):
_174
vacation_property = None
_174
form = ReservationForm()
_174
form.property_id.data = property_id
_174
_174
if request.method == 'POST':
_174
if form.validate_on_submit():
_174
guest = User.query.get(current_user.get_id())
_174
_174
vacation_property = VacationProperty.query.get(form.property_id.data)
_174
reservation = Reservation(form.message.data, vacation_property, guest)
_174
db.session.add(reservation)
_174
db.session.commit()
_174
_174
reservation.notify_host()
_174
_174
return redirect_to('properties')
_174
_174
if property_id is not None:
_174
vacation_property = VacationProperty.query.get(property_id)
_174
_174
return view_with_params('reservation', vacation_property=vacation_property, form=form)
_174
_174
_174
@app.route('/confirm', methods=["POST"])
_174
def confirm_reservation():
_174
form = ReservationConfirmationForm()
_174
sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."
_174
_174
user = User.query.filter(User.phone_number == form.From.data).first()
_174
reservation = Reservation \
_174
.query \
_174
.filter(Reservation.status == 'pending'
_174
and Reservation.vacation_property.host.id == user.id) \
_174
.first()
_174
_174
if reservation is not None:
_174
_174
if 'yes' in form.Body.data or 'accept' in form.Body.data:
_174
reservation.confirm()
_174
else:
_174
reservation.reject()
_174
_174
db.session.commit()
_174
_174
sms_response_text = "You have successfully {0} the reservation".format(reservation.status)
_174
reservation.notify_guest()
_174
_174
return twiml(_respond_message(sms_response_text))
_174
_174
_174
# controller utils
_174
@app.before_request
_174
def before_request():
_174
g.user = current_user
_174
uri_pattern = request.url_rule
_174
if current_user.is_authenticated and (
_174
uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):
_174
redirect_to('home')
_174
_174
_174
@login_manager.user_loader
_174
def load_user(id):
_174
try:
_174
return User.query.get(id)
_174
except:
_174
return None
_174
_174
_174
def _respond_message(message):
_174
response = VoiceResponse()
_174
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


_55
from airtng_flask.models import app_db, auth_token, account_sid, phone_number
_55
from flask import render_template
_55
from twilio.rest import Client
_55
_55
db = app_db()
_55
_55
_55
class Reservation(db.Model):
_55
__tablename__ = "reservations"
_55
_55
id = db.Column(db.Integer, primary_key=True)
_55
message = db.Column(db.String, nullable=False)
_55
status = db.Column(db.Enum('pending', 'confirmed', 'rejected', name='reservation_status_enum'), default='pending')
_55
_55
guest_id = db.Column(db.Integer, db.ForeignKey('users.id'))
_55
vacation_property_id = db.Column(db.Integer, db.ForeignKey('vacation_properties.id'))
_55
guest = db.relationship("User", back_populates="reservations")
_55
vacation_property = db.relationship("VacationProperty", back_populates="reservations")
_55
_55
def __init__(self, message, vacation_property, guest):
_55
self.message = message
_55
self.guest = guest
_55
self.vacation_property = vacation_property
_55
self.status = 'pending'
_55
_55
def confirm(self):
_55
self.status = 'confirmed'
_55
_55
def reject(self):
_55
self.status = 'rejected'
_55
_55
def __repr__(self):
_55
return '<VacationProperty %r %r>' % self.id, self.name
_55
_55
def notify_host(self):
_55
self._send_message(self.vacation_property.host.phone_number,
_55
render_template('messages/sms_host.txt',
_55
name=self.guest.name,
_55
description=self.vacation_property.description,
_55
message=self.message))
_55
_55
def notify_guest(self):
_55
self._send_message(self.guest.phone_number,
_55
render_template('messages/sms_guest.txt',
_55
description=self.vacation_property.description,
_55
status=self.status))
_55
_55
def _get_twilio_client(self):
_55
return Client(account_sid(), auth_token())
_55
_55
def _send_message(self, to, message):
_55
self._get_twilio_client().messages.create(
_55
to=to,
_55
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:


_10
http://<subdomain>.ngrok.io/confirm

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 he or she has any pending reservations. If she does, 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


_174
from airtng_flask import db, bcrypt, app, login_manager
_174
from flask import session, g, request, flash, Blueprint
_174
from flask_login import login_user, logout_user, current_user, login_required
_174
from twilio.twiml.voice_response import VoiceResponse
_174
_174
from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \
_174
ReservationConfirmationForm
_174
from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params
_174
from airtng_flask.models import init_models_module
_174
_174
init_models_module(db, bcrypt, app)
_174
_174
from airtng_flask.models.user import User
_174
from airtng_flask.models.vacation_property import VacationProperty
_174
from airtng_flask.models.reservation import Reservation
_174
_174
_174
@app.route('/', methods=["GET", "POST"])
_174
@app.route('/register', methods=["GET", "POST"])
_174
def register():
_174
form = RegisterForm()
_174
if request.method == 'POST':
_174
if form.validate_on_submit():
_174
_174
if User.query.filter(User.email == form.email.data).count() > 0:
_174
form.email.errors.append("Email address already in use.")
_174
return view('register', form)
_174
_174
user = User(
_174
name=form.name.data,
_174
email=form.email.data,
_174
password=form.password.data,
_174
phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data)
_174
)
_174
db.session.add(user)
_174
db.session.commit()
_174
login_user(user, remember=True)
_174
_174
return redirect_to('home')
_174
else:
_174
return view('register', form)
_174
_174
return view('register', form)
_174
_174
_174
@app.route('/login', methods=["GET", "POST"])
_174
def login():
_174
form = LoginForm()
_174
if request.method == 'POST':
_174
if form.validate_on_submit():
_174
candidate_user = User.query.filter(User.email == form.email.data).first()
_174
_174
if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,
_174
form.password.data):
_174
form.password.errors.append("Invalid credentials.")
_174
return view('login', form)
_174
_174
login_user(candidate_user, remember=True)
_174
return redirect_to('home')
_174
return view('login', form)
_174
_174
_174
@app.route('/logout', methods=["POST"])
_174
@login_required
_174
def logout():
_174
logout_user()
_174
return redirect_to('home')
_174
_174
_174
@app.route('/home', methods=["GET"])
_174
@login_required
_174
def home():
_174
return view('home')
_174
_174
_174
@app.route('/properties', methods=["GET"])
_174
@login_required
_174
def properties():
_174
vacation_properties = VacationProperty.query.all()
_174
return view_with_params('properties', vacation_properties=vacation_properties)
_174
_174
_174
@app.route('/properties/new', methods=["GET", "POST"])
_174
@login_required
_174
def new_property():
_174
form = VacationPropertyForm()
_174
if request.method == 'POST':
_174
if form.validate_on_submit():
_174
host = User.query.get(current_user.get_id())
_174
_174
property = VacationProperty(form.description.data, form.image_url.data, host)
_174
db.session.add(property)
_174
db.session.commit()
_174
return redirect_to('properties')
_174
_174
return view('property_new', form)
_174
_174
_174
@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})
_174
@app.route('/reservations/<property_id>', methods=["GET", "POST"])
_174
@login_required
_174
def new_reservation(property_id):
_174
vacation_property = None
_174
form = ReservationForm()
_174
form.property_id.data = property_id
_174
_174
if request.method == 'POST':
_174
if form.validate_on_submit():
_174
guest = User.query.get(current_user.get_id())
_174
_174
vacation_property = VacationProperty.query.get(form.property_id.data)
_174
reservation = Reservation(form.message.data, vacation_property, guest)
_174
db.session.add(reservation)
_174
db.session.commit()
_174
_174
reservation.notify_host()
_174
_174
return redirect_to('properties')
_174
_174
if property_id is not None:
_174
vacation_property = VacationProperty.query.get(property_id)
_174
_174
return view_with_params('reservation', vacation_property=vacation_property, form=form)
_174
_174
_174
@app.route('/confirm', methods=["POST"])
_174
def confirm_reservation():
_174
form = ReservationConfirmationForm()
_174
sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."
_174
_174
user = User.query.filter(User.phone_number == form.From.data).first()
_174
reservation = Reservation \
_174
.query \
_174
.filter(Reservation.status == 'pending'
_174
and Reservation.vacation_property.host.id == user.id) \
_174
.first()
_174
_174
if reservation is not None:
_174
_174
if 'yes' in form.Body.data or 'accept' in form.Body.data:
_174
reservation.confirm()
_174
else:
_174
reservation.reject()
_174
_174
db.session.commit()
_174
_174
sms_response_text = "You have successfully {0} the reservation".format(reservation.status)
_174
reservation.notify_guest()
_174
_174
return twiml(_respond_message(sms_response_text))
_174
_174
_174
# controller utils
_174
@app.before_request
_174
def before_request():
_174
g.user = current_user
_174
uri_pattern = request.url_rule
_174
if current_user.is_authenticated and (
_174
uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):
_174
redirect_to('home')
_174
_174
_174
@login_manager.user_loader
_174
def load_user(id):
_174
try:
_174
return User.query.get(id)
_174
except:
_174
return None
_174
_174
_174
def _respond_message(message):
_174
response = VoiceResponse()
_174
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 Twilio makes it easy to 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.

Thanks for checking out this tutorial! If you have any feedback to share with us, please hit us up on Twitter(link takes you to an external page) and let us know what you're building!


Rate this page: