diff --git a/bbcli/cli.py b/bbcli/cli.py
index ac376cadf84b8e0ebe6a9fc4b389b3388737d7de..29bfc866e832332572915a75c3cc4895e625ac2c 100644
--- a/bbcli/cli.py
+++ b/bbcli/cli.py
@@ -9,8 +9,8 @@ import click
 
 from bbcli.commands.courses import list_courses
 from bbcli.commands.announcements import list_announcements, create_announcement, delete_announcement, update_announcement
-from bbcli.commands.contents import create_assignment, create_courselink, create_folder, delete_content, list_contents, create_document, create_file, create_web_link, update_content, upload_attachment, get_content
-from bbcli.commands.assignments import get_assignments
+from bbcli.commands.contents import create_assignment_from_contents, create_courselink, create_folder, delete_content, list_contents, create_document, create_file, create_web_link, update_content, upload_attachment, get_content
+from bbcli.commands.assignments import get_assignments, submit_attempt, grade_assignment, get_attempts, get_attempt, submit_draft, update_attempt, submit_draft, create_assignment
 from bbcli.services.authorization_service import login
 
 load_dotenv()
@@ -100,6 +100,23 @@ def assignments(ctx):
 
 assignments.add_command(get_assignments)
 assignments.add_command(create_assignment)
+assignments.add_command(grade_assignment)
+
+
+@assignments.group()
+@click.pass_context
+def attempts(ctx):
+    """
+    Commands for creating, submitting and listing attempts for an assignment.
+    """
+    pass
+
+
+attempts.add_command(get_attempts)
+attempts.add_command(get_attempt)
+attempts.add_command(submit_attempt)
+attempts.add_command(submit_draft)
+attempts.add_command(update_attempt)
 
 """
 CONTENT COMMANDS ENTRY POINT
@@ -140,4 +157,4 @@ create.add_command(create_file)
 create.add_command(create_folder)
 create.add_command(create_courselink)
 create.add_command(upload_attachment)
-create.add_command(create_assignment)
+create.add_command(create_assignment_from_contents)
diff --git a/bbcli/commands/assignments.py b/bbcli/commands/assignments.py
index 429cdb605b5010eb2c72c2b1599a7c0216051891..f3e90bd84db6292fcd36273d92bffd41329263a1 100644
--- a/bbcli/commands/assignments.py
+++ b/bbcli/commands/assignments.py
@@ -1,14 +1,151 @@
-from email.policy import default
 import click
+from bbcli.commands.contents import grading_options, set_dates, standard_options
+from bbcli.entities.content_builder_entitites import GradingOptions, StandardOptions
+from bbcli.services import assignment_service, contents_service
+from bbcli.utils.error_handler import exception_handler
+from bbcli.utils.utils import format_date
 
-from bbcli.services import assignment_service
 
+def attempt_options(function):
+    function = click.option(
+        '--status', default='Completed', help='The status of this attempt.', show_default=True)(function)
+    function = click.option(
+        '--text', help='The text grade associated with this attempt.')(function)
+    function = click.option(
+        '--score', type=int, help='The score associated with this attempt.')(function)
+    function = click.option(
+        '--notes', help='The instructor notes associated with this attempt.')(function)
+    function = click.option(
+        '--feedback', help='The instructor feedback associated with this attempt.')(function)
+    function = click.option('--exempt', is_flag=True,
+                            help='Whether the score associated with this attempt is ignored when computing the user\'s grade for the associated grade column.')(function)
+    return function
 
-@click.command(name='get')
-@click.argument('course_id', required=True)
+
+# TODO: This function is a copy of the same function in contents.py. Fix this.
+@click.command(name='create')
+@click.argument('course_id', required=True, type=str)
+@click.argument('parent_id', required=True, type=str)
+@click.argument('title', required=True, type=str)
+@click.argument('attachments', required=False, nargs=-1, type=click.Path())
+@standard_options
+@grading_options
+@click.pass_context
+@exception_handler
+def create_assignment(ctx, course_id: str, parent_id: str, title: str,
+                      hide_content: bool, reviewable: bool,
+                      start_date: str, end_date: str,
+                      due_date: str, max_attempts: int, unlimited_attempts: bool, score: int,
+                      attachments: tuple):
+    """
+    Create an assignment.
+    """
+    standard_options = StandardOptions(hide_content, reviewable)
+    grading_options = GradingOptions(
+        attempts_allowed=max_attempts, is_unlimited_attemps_allowed=unlimited_attempts, score_possible=score)
+
+    set_dates(standard_options, start_date, end_date)
+    grading_options.due = format_date(due_date)
+
+    response = contents_service.create_assignment(
+        ctx.obj['SESSION'], course_id, parent_id, title, standard_options, grading_options, attachments)
+    click.echo(response)
+
+
+@click.command(name='list')
+@click.argument('course-id', required=True)
 @click.pass_context
 def get_assignments(ctx, course_id):
     """
-    Get assignments
+    List assignments for a course.
     """
     assignment_service.get_assignments(ctx.obj['SESSION'], course_id)
+
+
+@click.command(name='list')
+@click.argument('course_id', required=True)
+@click.argument('column_id', required=True)
+@click.option('--submitted', is_flag=True)
+@click.pass_context
+def get_attempts(ctx, course_id, column_id, submitted):
+    """
+    List attempts for an assignment.
+    """
+    assignment_service.get_column_attempts(
+        ctx.obj['SESSION'], course_id, column_id, print_submitted=submitted)
+
+
+# TODO: Retrieve the submission w/ attachments.
+@click.command(name='get')
+@click.argument('course_id', required=True)
+@click.argument('column_id', required=True)
+@click.argument('attempt_id', required=True)
+@click.pass_context
+def get_attempt(ctx, course_id, column_id, attempt_id):
+    """
+    Get a specific attempt for an assignment.
+    """
+    assignment_service.get_column_attempt(
+        ctx.obj['SESSION'], course_id, column_id, attempt_id)
+
+
+@click.command(name='submit')
+@click.argument('course_id', required=True)
+@click.argument('column_id', required=True)
+@click.option('--studentComments', help='The student comments associated with this attempt.')
+@click.option('--studentSubmission', help='The student submission text associated with this attempt.')
+@click.option('--file', help='Attach a file to an attempt for a Student Submission. Relative path of file.')
+@click.option('--draft', is_flag=True)
+@click.pass_context
+def submit_attempt(ctx, course_id, column_id, studentComments, studentSubmission, file, draft):
+    """
+    Submit assignment attempt.
+    """
+    assignment_service.create_column_attempt(
+        ctx.obj['SESSION'], course_id, column_id, studentComments=studentComments, studentSubmission=studentSubmission, dst=file, status='needsGrading', draft=draft)
+
+
+@click.command(name='submit-draft')
+@click.argument('course_id', required=True)
+@click.argument('column_id', required=True)
+@click.argument('attempt_id', required=True)
+@click.pass_context
+def submit_draft(ctx, course_id, column_id, attempt_id):
+    """
+    Submit assignment draft.
+    """
+    assignment_service.update_column_attempt(
+        ctx.obj['SESSION'], course_id=course_id, column_id=column_id, attempt_id=attempt_id, status='needsGrading')
+
+
+@click.command(name='update')
+@click.argument('course_id', required=True)
+@click.argument('column_id', required=True)
+@click.argument('attempt_id', required=True)
+@attempt_options
+@click.option('--studentComments', help='The student comments associated with this attempt.')
+@click.option('--studentSubmission', help='The student submission text associated with this attempt.')
+@click.pass_context
+def update_attempt(ctx, course_id, column_id, attempt_id, status, comments, submission, file):
+    """
+    Update assignment.
+    """
+    assignment_service.update_column_attempt(
+        session=ctx.obj['SESSION'], course_id=course_id, column_id=column_id, attempt_id=attempt_id, status=status, studentComments=comments, studentSubmission=submission, dst=file)
+
+
+@click.command(name='grade')
+@click.argument('course_id', required=True)
+@click.argument('column_id', required=True)
+@click.argument('attempt_id', required=True)
+@attempt_options
+@click.pass_context
+def grade_assignment(ctx, course_id, column_id, attempt_id, status, score, text, notes, feedback, exempt):
+    """
+    Grade assignment.
+    """
+    if status is None:
+        status = 'Completed'
+
+    assignment_service.update_column_attempt(session=ctx.obj['SESSION'], status=status, course_id=course_id, column_id=column_id,
+                                             attempt_id=attempt_id, score=score, text=text, notes=notes, feedback=feedback, exempt=exempt)
diff --git a/bbcli/commands/contents.py b/bbcli/commands/contents.py
index 3e62f3e3d108596e709dc0c38d2cfcda707345f9..872c27819edf25db06ecf3b21eccee718f8f1181 100644
--- a/bbcli/commands/contents.py
+++ b/bbcli/commands/contents.py
@@ -269,7 +269,7 @@ def create_courselink(ctx, course_id: str, parent_id: str, title: str, target_id
 @grading_options
 @click.pass_context
 @exception_handler
-def create_assignment(ctx, course_id: str, parent_id: str, title: str,
+def create_assignment_from_contents(ctx, course_id: str, parent_id: str, title: str,
                       hide_content: bool, reviewable: bool,
                       start_date: str, end_date: str,
                       due_date: str, max_attempts: int, unlimited_attempts: bool, score: int,
diff --git a/bbcli/services/assignment_service.py b/bbcli/services/assignment_service.py
index e4755c5d3d4b2031bd6dab98b84410c8aafa6425..364d25e6c51900819a62127db8c88e810c448cde 100644
--- a/bbcli/services/assignment_service.py
+++ b/bbcli/services/assignment_service.py
@@ -3,6 +3,9 @@ import json
 import click
 import requests
 import dateutil.parser
+from bbcli.services.contents_service import upload_file
+from bbcli.services.utils.attempt_builder import AttemptBuilder
+from tabulate import tabulate
 
 from bbcli.utils.URL_builder import URL_builder
 
@@ -20,18 +23,129 @@ def get_assignments(session: requests.Session, course_id):
 
 def print_assignments(assignments):
     for i in range(len(assignments)):
+        column_id = assignments[i]['id']
         name = assignments[i]['name']
         due = 'N/A'
-        if ('grading' in assignments[i]):
-            if ('due' in assignments[i]['grading']):
+        if 'grading' in assignments[i]:
+            if 'due' in assignments[i]['grading']:
                 due = assignments[i]['grading']['due']
                 due_datetime = utc_to_local(dateutil.parser.parse(due))
                 date = str(due_datetime.date())
                 time = str(due_datetime.time())
                 due = f'{date} {time}'
 
-        click.echo('{:<40s} due {:<10s}'.format(name, due))
+        click.echo('{:<12s}{:<40s} due {:<10s}'.format(column_id, name, due))
 
 
 def utc_to_local(utc_dt):
     return utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None)
+
+
+def get_column_attempts(session: requests.Session, course_id, column_id, print_submitted):
+    url = url_builder.base_v2().add_courses().add_id(course_id).add_gradebook(
+    ).add_columns().add_id(column_id).add_attempts().create()
+
+    response = session.get(url)
+    response = json.loads(response.text)
+    results = response['results']
+
+    if print_submitted:
+        print_submitted_attempts(results)
+    else:
+        print_all_attempts(results)
+
+
+def print_submitted_attempts(attempts):
+    table = {'id': [], 'user id': [], 'status': [], 'score': [], 'created': []}
+    statuses = ['NeedsGrading', 'Completed']
+    for attempt in attempts:
+        for status in statuses:
+            if (status == attempt['status']):
+                append_to_table(attempt, table)
+                continue
+
+    click.echo(tabulate(table, headers='keys'))
+
+
+def print_all_attempts(attempts):
+    table = {'id': [], 'user id': [], 'status': [], 'score': [], 'created': []}
+    for attempt in attempts:
+        append_to_table(attempt, table)
+    click.echo(tabulate(table, headers='keys'))
+
+
+def append_to_table(attempt, table):
+    table['id'].append(attempt['id'])
+    table['user id'].append(attempt['userId'])
+    table['status'].append(attempt['status'])
+    table['score'].append(
+        attempt['score']) if 'score' in attempt else table['score'].append('N/A')
+    created = utc_to_local(dateutil.parser.parse(attempt['created']))
+    table['created'].append(created)
+
+
+def get_column_attempt(session: requests.Session, course_id, column_id, attempt_id):
+    url = url_builder.base_v2().add_courses().add_id(course_id).add_gradebook(
+    ).add_columns().add_id(column_id).add_attempts().add_id(attempt_id).create()
+
+    response = session.get(url)
+    attempt = json.loads(response.text)
+    attempt = json.dumps(attempt, indent=2)
+    click.echo(attempt)
+
+
+def create_column_attempt(session: requests.Session, course_id, column_id, studentComments=None, studentSubmission=None, dst: str = None, status=None, draft: bool = False):
+    url = url_builder.base_v2().add_courses().add_id(course_id).add_gradebook(
+    ).add_columns().add_id(column_id).add_attempts().create()
+
+    if draft or dst is not None:
+        status = 'InProgress'
+
+    attempt = AttemptBuilder(studentComments=studentComments,
+                             studentSubmission=studentSubmission,
+                             status=status)
+    data = attempt.create_json()
+    json_data = json.dumps(data, indent=2)
+    response = session.post(url, data=json_data)
+    response_json = json.loads(response.text)
+    click.echo(response_json)
+
+    if dst is not None and response.status_code == 201:
+        attempt_id = response_json['id']
+        attach_file(session, course_id, attempt_id, dst)
+        if draft:
+            return
+        update_column_attempt(session, course_id, column_id,
+                              attempt_id, status='NeedsGrading')
+
+
+def update_column_attempt(session: requests.Session, course_id, column_id, attempt_id, status=None, score=None, text=None, notes=None, feedback=None, studentComments=None, studentSubmission=None, exempt=None, dst=None):
+
+    url = url_builder.base_v2().add_courses().add_id(course_id).add_gradebook(
+    ).add_columns().add_id(column_id).add_attempts().add_id(attempt_id).create()
+
+    attempt = AttemptBuilder(status=status, score=score, text=text,
+                             notes=notes, feedback=feedback, studentComments=studentComments, studentSubmission=studentSubmission, exempt=exempt)
+    data = attempt.create_json()
+    json_data = json.dumps(data, indent=2)
+
+    response = session.patch(url, data=json_data)
+    response = json.loads(response.text)
+    click.echo(response)
+
+    if dst is not None:
+        attach_file(session, course_id, attempt_id, dst)
+
+
+def attach_file(session: requests.Session, course_id, attempt_id, dst: str):
+    url = url_builder.base_v1().add_courses().add_id(
+        course_id).add_gradebook().add_attempts().add_id(attempt_id).add_files().create()
+
+    uploaded_file = upload_file(session, dst)
+
+    data = {'uploadId': uploaded_file['id']}
+    data = json.dumps(data)
+    response = session.post(url, data)
+
+    response_json = json.loads(response.text)
+    click.echo(response_json)
diff --git a/bbcli/services/utils/attempt_builder.py b/bbcli/services/utils/attempt_builder.py
new file mode 100644
index 0000000000000000000000000000000000000000..e788a844122c496d12517c31096ccaa3d34153dc
--- /dev/null
+++ b/bbcli/services/utils/attempt_builder.py
@@ -0,0 +1,46 @@
+class AttemptBuilder(object):
+    def __init__(self, status=None, score=None, text=None, notes=None, feedback=None, studentComments=None, studentSubmission=None, exempt=None) -> None:
+        self.status = status
+        self.score = score
+        self.text = text
+        self.notes = notes
+        self.feedback = feedback
+        self.studentComments = studentComments
+        self.studentSubmission = studentSubmission
+        self.exempt = exempt
+
+    def create_json(self):
+        attempt = {}
+        if self.status:
+            attempt.update({
+                'status': self.status
+            })
+        if self.score:
+            attempt.update({
+                'score': self.score
+            })
+        if self.text:
+            attempt.update({
+                'text': self.text
+            })
+        if self.notes:
+            attempt.update({
+                'notes': self.notes
+            })
+        if self.feedback:
+            attempt.update({
+                'feedback': self.feedback
+            })
+        if self.studentComments:
+            attempt.update({
+                'studentComments': self.studentComments
+            })
+        if self.studentSubmission:
+            attempt.update({
+                'studentSubmission': self.studentSubmission
+            })
+        if self.exempt:
+            attempt.update({
+                'exempt': self.exempt
+            })
+        return attempt
diff --git a/bbcli/utils/URL_builder.py b/bbcli/utils/URL_builder.py
index 337ce96a18095091b827189fa2a5e0764a1d2501..12c72add6d9ea05ebedcbab3a11b130afed64ba7 100644
--- a/bbcli/utils/URL_builder.py
+++ b/bbcli/utils/URL_builder.py
@@ -82,6 +82,18 @@ class Builder(ABC):
     def add_columns(self) -> Builder:
         pass
 
+    @abstractmethod
+    def add_attempts(self) -> Builder:
+        pass
+
+    @abstractmethod
+    def add_files(self) -> Builder:
+        pass
+
+    @abstractmethod
+    def create(self) -> Builder:
+        pass
+
 
 class URL_builder(Builder):
 
@@ -164,6 +176,14 @@ class URL_builder(Builder):
         self._product.add('/columns')
         return self
 
+    def add_attempts(self) -> Builder:
+        self._product.add('/attempts')
+        return self
+    
+    def add_files(self) -> Builder:
+        self._product.add('/files')
+        return self
+
     def create(self) -> str:
         url = self._product.get_url()
         self._product = URL()