# 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.](https://customers.twilio.com/en-us/airbnb)*

## Workflow Building Blocks

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

```py title="Load the application configuration" description="airtng_flask/__init__.py"
import os
from airtng_flask.config import config_env_files
from flask import Flask

from flask_bcrypt import Bcrypt
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager

db = SQLAlchemy()
bcrypt = Bcrypt()
login_manager = LoginManager()


def create_app(config_name='development', p_db=db, p_bcrypt=bcrypt, p_login_manager=login_manager):
    new_app = Flask(__name__)
    config_app(config_name, new_app)
    new_app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    p_db.init_app(new_app)
    p_bcrypt.init_app(new_app)
    p_login_manager.init_app(new_app)
    p_login_manager.login_view = 'register'
    return new_app


def config_app(config_name, new_app):
    new_app.config.from_object(config_env_files[config_name])


app = create_app()

import airtng_flask.views
```

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

## User and Session Management

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.

```py title="The Airtng User model" description="airtng_flask/models/user.py"
# !mark(8:48)
from airtng_flask.models import app_db
from airtng_flask.models import bcrypt

db = app_db()
bcrypt = bcrypt()


class User(db.Model):
    __tablename__ = "users"

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String, nullable=False)
    email = db.Column(db.String, nullable=False)
    password = db.Column(db.String, nullable=False)
    phone_number = db.Column(db.String, nullable=False)

    reservations = db.relationship("Reservation", back_populates="guest")
    vacation_properties = db.relationship("VacationProperty", back_populates="host")

    def __init__(self, name, email, password, phone_number):
        self.name = name
        self.email = email
        self.password = bcrypt.generate_password_hash(password)
        self.phone_number = phone_number

    def is_authenticated(self):
        return True

    def is_active(self):
        return True

    def is_anonymous(self):
        return False

    def get_id(self):
        try:
            return unicode(self.id)
        except NameError:
            return str(self.id)

    # Python 3

    def __unicode__(self):
        return self.name

    def __repr__(self):
        return '<User %r>' % (self.name)
```

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

## The VacationProperty Model

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.

```py title="VacationProperty model" description="airtng_flask/models/vacation_property.py"
# !mark(6:23)
from airtng_flask.models import app_db

db = app_db()


class VacationProperty(db.Model):
    __tablename__ = "vacation_properties"

    id = db.Column(db.Integer, primary_key=True)
    description = db.Column(db.String, nullable=False)
    image_url = db.Column(db.String, nullable=False)

    host_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    host = db.relationship("User", back_populates="vacation_properties")
    reservations = db.relationship("Reservation", back_populates="vacation_property")

    def __init__(self, description, image_url, host):
        self.description = description
        self.image_url = image_url
        self.host = host

    def __repr__(self):
        return '<VacationProperty %r %r>' % self.id, self.description
```

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

## The Reservation Model

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`

```py title="The Reservation model" description="airtng_flask/models/reservation.py"
# !mark(8:41)
from airtng_flask.models import app_db, auth_token, account_sid, phone_number
from flask import render_template
from twilio.rest import Client

db = app_db()


class Reservation(db.Model):
    __tablename__ = "reservations"

    id = db.Column(db.Integer, primary_key=True)
    message = db.Column(db.String, nullable=False)
    status = db.Column(db.Enum('pending', 'confirmed', 'rejected', name='reservation_status_enum'), default='pending')

    guest_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    vacation_property_id = db.Column(db.Integer, db.ForeignKey('vacation_properties.id'))
    guest = db.relationship("User", back_populates="reservations")
    vacation_property = db.relationship("VacationProperty", back_populates="reservations")

    def __init__(self, message, vacation_property, guest):
        self.message = message
        self.guest = guest
        self.vacation_property = vacation_property
        self.status = 'pending'

    def confirm(self):
        self.status = 'confirmed'

    def reject(self):
        self.status = 'rejected'

    def __repr__(self):
        return '<VacationProperty %r %r>' % self.id, self.name

    def notify_host(self):
        self._send_message(self.vacation_property.host.phone_number,
                           render_template('messages/sms_host.txt',
                                           name=self.guest.name,
                                           description=self.vacation_property.description,
                                           message=self.message))

    def notify_guest(self):
        self._send_message(self.guest.phone_number,
                           render_template('messages/sms_guest.txt',
                                           description=self.vacation_property.description,
                                           status=self.status))

    def _get_twilio_client(self):
        return Client(account_sid(), auth_token())

    def _send_message(self, to, message):
        self._get_twilio_client().messages.create(
                to=to,
                from_=phone_number(),
                body=message)
```

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

## Creating 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.

```py title="Routes for the Airtng workflow" description="airtng_flask/views.py"
# !mark(99:125)
from airtng_flask import db, bcrypt, app, login_manager
from flask import session, g, request, flash, Blueprint
from flask_login import login_user, logout_user, current_user, login_required
from twilio.twiml.voice_response import VoiceResponse

from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \
    ReservationConfirmationForm
from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params
from airtng_flask.models import init_models_module

init_models_module(db, bcrypt, app)

from airtng_flask.models.user import User
from airtng_flask.models.vacation_property import VacationProperty
from airtng_flask.models.reservation import Reservation


@app.route('/', methods=["GET", "POST"])
@app.route('/register', methods=["GET", "POST"])
def register():
    form = RegisterForm()
    if request.method == 'POST':
        if form.validate_on_submit():

            if User.query.filter(User.email == form.email.data).count() > 0:
                form.email.errors.append("Email address already in use.")
                return view('register', form)

            user = User(
                    name=form.name.data,
                    email=form.email.data,
                    password=form.password.data,
                    phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data)
            )
            db.session.add(user)
            db.session.commit()
            login_user(user, remember=True)

            return redirect_to('home')
        else:
            return view('register', form)

    return view('register', form)


@app.route('/login', methods=["GET", "POST"])
def login():
    form = LoginForm()
    if request.method == 'POST':
        if form.validate_on_submit():
            candidate_user = User.query.filter(User.email == form.email.data).first()

            if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,
                                                                        form.password.data):
                form.password.errors.append("Invalid credentials.")
                return view('login', form)

            login_user(candidate_user, remember=True)
            return redirect_to('home')
    return view('login', form)


@app.route('/logout', methods=["POST"])
@login_required
def logout():
    logout_user()
    return redirect_to('home')


@app.route('/home', methods=["GET"])
@login_required
def home():
    return view('home')


@app.route('/properties', methods=["GET"])
@login_required
def properties():
    vacation_properties = VacationProperty.query.all()
    return view_with_params('properties', vacation_properties=vacation_properties)


@app.route('/properties/new', methods=["GET", "POST"])
@login_required
def new_property():
    form = VacationPropertyForm()
    if request.method == 'POST':
        if form.validate_on_submit():
            host = User.query.get(current_user.get_id())

            property = VacationProperty(form.description.data, form.image_url.data, host)
            db.session.add(property)
            db.session.commit()
            return redirect_to('properties')

    return view('property_new', form)


@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})
@app.route('/reservations/<property_id>', methods=["GET", "POST"])
@login_required
def new_reservation(property_id):
    vacation_property = None
    form = ReservationForm()
    form.property_id.data = property_id

    if request.method == 'POST':
        if form.validate_on_submit():
            guest = User.query.get(current_user.get_id())

            vacation_property = VacationProperty.query.get(form.property_id.data)
            reservation = Reservation(form.message.data, vacation_property, guest)
            db.session.add(reservation)
            db.session.commit()

            reservation.notify_host()

            return redirect_to('properties')

    if property_id is not None:
        vacation_property = VacationProperty.query.get(property_id)

    return view_with_params('reservation', vacation_property=vacation_property, form=form)


@app.route('/confirm', methods=["POST"])
def confirm_reservation():
    form = ReservationConfirmationForm()
    sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."

    user = User.query.filter(User.phone_number == form.From.data).first()
    reservation = Reservation \
        .query \
        .filter(Reservation.status == 'pending'
                and Reservation.vacation_property.host.id == user.id) \
        .first()

    if reservation is not None:

        if 'yes' in form.Body.data or 'accept' in form.Body.data:
            reservation.confirm()
        else:
            reservation.reject()

        db.session.commit()

        sms_response_text = "You have successfully {0} the reservation".format(reservation.status)
        reservation.notify_guest()

    return twiml(_respond_message(sms_response_text))


# controller utils
@app.before_request
def before_request():
    g.user = current_user
    uri_pattern = request.url_rule
    if current_user.is_authenticated and (
                        uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):
        redirect_to('home')


@login_manager.user_loader
def load_user(id):
    try:
        return User.query.get(id)
    except:
        return None


def _respond_message(message):
    response = VoiceResponse()
    response.message(message)
    return response
```

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

## Notify the Host via SMS

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](https://twilio.com/console).

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.

```py title="Notify the user and the host" description="airtng_flask/models/reservation.py"
# !mark(35:55)
from airtng_flask.models import app_db, auth_token, account_sid, phone_number
from flask import render_template
from twilio.rest import Client

db = app_db()


class Reservation(db.Model):
    __tablename__ = "reservations"

    id = db.Column(db.Integer, primary_key=True)
    message = db.Column(db.String, nullable=False)
    status = db.Column(db.Enum('pending', 'confirmed', 'rejected', name='reservation_status_enum'), default='pending')

    guest_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    vacation_property_id = db.Column(db.Integer, db.ForeignKey('vacation_properties.id'))
    guest = db.relationship("User", back_populates="reservations")
    vacation_property = db.relationship("VacationProperty", back_populates="reservations")

    def __init__(self, message, vacation_property, guest):
        self.message = message
        self.guest = guest
        self.vacation_property = vacation_property
        self.status = 'pending'

    def confirm(self):
        self.status = 'confirmed'

    def reject(self):
        self.status = 'rejected'

    def __repr__(self):
        return '<VacationProperty %r %r>' % self.id, self.name

    def notify_host(self):
        self._send_message(self.vacation_property.host.phone_number,
                           render_template('messages/sms_host.txt',
                                           name=self.guest.name,
                                           description=self.vacation_property.description,
                                           message=self.message))

    def notify_guest(self):
        self._send_message(self.guest.phone_number,
                           render_template('messages/sms_guest.txt',
                                           description=self.vacation_property.description,
                                           status=self.status))

    def _get_twilio_client(self):
        return Client(account_sid(), auth_token())

    def _send_message(self, to, message):
        self._get_twilio_client().messages.create(
                to=to,
                from_=phone_number(),
                body=message)
```

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

## Handle Incoming Twilio Requests

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

In the [Twilio console](https://twilio.com/console), you should change the 'A Message Comes In' webhook to call your application's endpoint in the route `/confirm:`

![Messaging configuration with webhook URL set to https://yourserver.com/sms.](https://docs-resources.prod.twilio.com/f25966b33e960a4214a186c1dbdd0d35aff94f3b3c704f7db85ba8a082ec20d2.png)

One way to expose your machine to the world during development is using [ngrok](https://ngrok.com/). Your URL for the SMS web hook on your phone number should look something like this:

```bash
http://<subdomain>.ngrok.io/confirm

```

An incoming request from Twilio comes with some helpful [parameters](/docs/messaging/twiml#data-formats). 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.

#### TwiML Response

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.*

```py title="Confirm or reject a pending reservation request" description="airtng_flask/views.py"
# !mark(126:150)
from airtng_flask import db, bcrypt, app, login_manager
from flask import session, g, request, flash, Blueprint
from flask_login import login_user, logout_user, current_user, login_required
from twilio.twiml.voice_response import VoiceResponse

from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \
    ReservationConfirmationForm
from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params
from airtng_flask.models import init_models_module

init_models_module(db, bcrypt, app)

from airtng_flask.models.user import User
from airtng_flask.models.vacation_property import VacationProperty
from airtng_flask.models.reservation import Reservation


@app.route('/', methods=["GET", "POST"])
@app.route('/register', methods=["GET", "POST"])
def register():
    form = RegisterForm()
    if request.method == 'POST':
        if form.validate_on_submit():

            if User.query.filter(User.email == form.email.data).count() > 0:
                form.email.errors.append("Email address already in use.")
                return view('register', form)

            user = User(
                    name=form.name.data,
                    email=form.email.data,
                    password=form.password.data,
                    phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data)
            )
            db.session.add(user)
            db.session.commit()
            login_user(user, remember=True)

            return redirect_to('home')
        else:
            return view('register', form)

    return view('register', form)


@app.route('/login', methods=["GET", "POST"])
def login():
    form = LoginForm()
    if request.method == 'POST':
        if form.validate_on_submit():
            candidate_user = User.query.filter(User.email == form.email.data).first()

            if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,
                                                                        form.password.data):
                form.password.errors.append("Invalid credentials.")
                return view('login', form)

            login_user(candidate_user, remember=True)
            return redirect_to('home')
    return view('login', form)


@app.route('/logout', methods=["POST"])
@login_required
def logout():
    logout_user()
    return redirect_to('home')


@app.route('/home', methods=["GET"])
@login_required
def home():
    return view('home')


@app.route('/properties', methods=["GET"])
@login_required
def properties():
    vacation_properties = VacationProperty.query.all()
    return view_with_params('properties', vacation_properties=vacation_properties)


@app.route('/properties/new', methods=["GET", "POST"])
@login_required
def new_property():
    form = VacationPropertyForm()
    if request.method == 'POST':
        if form.validate_on_submit():
            host = User.query.get(current_user.get_id())

            property = VacationProperty(form.description.data, form.image_url.data, host)
            db.session.add(property)
            db.session.commit()
            return redirect_to('properties')

    return view('property_new', form)


@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})
@app.route('/reservations/<property_id>', methods=["GET", "POST"])
@login_required
def new_reservation(property_id):
    vacation_property = None
    form = ReservationForm()
    form.property_id.data = property_id

    if request.method == 'POST':
        if form.validate_on_submit():
            guest = User.query.get(current_user.get_id())

            vacation_property = VacationProperty.query.get(form.property_id.data)
            reservation = Reservation(form.message.data, vacation_property, guest)
            db.session.add(reservation)
            db.session.commit()

            reservation.notify_host()

            return redirect_to('properties')

    if property_id is not None:
        vacation_property = VacationProperty.query.get(property_id)

    return view_with_params('reservation', vacation_property=vacation_property, form=form)


@app.route('/confirm', methods=["POST"])
def confirm_reservation():
    form = ReservationConfirmationForm()
    sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."

    user = User.query.filter(User.phone_number == form.From.data).first()
    reservation = Reservation \
        .query \
        .filter(Reservation.status == 'pending'
                and Reservation.vacation_property.host.id == user.id) \
        .first()

    if reservation is not None:

        if 'yes' in form.Body.data or 'accept' in form.Body.data:
            reservation.confirm()
        else:
            reservation.reject()

        db.session.commit()

        sms_response_text = "You have successfully {0} the reservation".format(reservation.status)
        reservation.notify_guest()

    return twiml(_respond_message(sms_response_text))


# controller utils
@app.before_request
def before_request():
    g.user = current_user
    uri_pattern = request.url_rule
    if current_user.is_authenticated and (
                        uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):
        redirect_to('home')


@login_manager.user_loader
def load_user(id):
    try:
        return User.query.get(id)
    except:
        return None


def _respond_message(message):
    response = VoiceResponse()
    response.message(message)
    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.

## Where to Next?

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](/docs/messaging/tutorials/masked-numbers/python).

You might also enjoy these other tutorials:

**[IVR Phone Tree](/docs/voice/tutorials/build-interactive-voice-response-ivr-phone-tree/python)**

Create a seamless customer service experience by building an IVR ([Interactive Voice Response](/docs/glossary/what-is-ivr)) Phone Tree for your company.

**[Click To Call](/docs/voice/sdks/javascript/get-started)**

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