diff --git a/CHANGELOG.MD b/CHANGELOG.MD
index 92aa8784f..e12ec525a 100644
--- a/CHANGELOG.MD
+++ b/CHANGELOG.MD
@@ -1,6 +1,10 @@
 # Change Log
 
-All notable changes to this project will be documented in this file.
+All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/).
+
+## December 28, 2023
+
+> **Feature**: Added the timeline widget. [🎟️DESENG-439](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-439)
 
 ## December 11, 2023
 
diff --git a/analytics-api/src/analytics_api/config.py b/analytics-api/src/analytics_api/config.py
index 115518b40..269b4a411 100644
--- a/analytics-api/src/analytics_api/config.py
+++ b/analytics-api/src/analytics_api/config.py
@@ -28,7 +28,7 @@
 load_dotenv(find_dotenv())
 
 
-def get_named_config(environment: str | None) -> '_Config':
+def get_named_config(environment: 'str | None') -> '_Config':
     """
     Retrieve a configuration object by name. Used by the Flask app factory.
 
diff --git a/docs/MET_database_ERD.md b/docs/MET_database_ERD.md
index db6470461..afd7018f1 100644
--- a/docs/MET_database_ERD.md
+++ b/docs/MET_database_ERD.md
@@ -130,6 +130,33 @@ erDiagram
         string updated_by
     }
     widget only one to zero or more widget_video : has 
+    widget_timeline {
+        integer id PK
+        integer widget_id FK "The id from widget"
+        integer engagement_id FK "The id from engagement"
+        string title
+        string description
+        timestamp created_date
+        timestamp updated_date        
+        string created_by
+        string updated_by
+    }
+    widget only one to zero or more widget_timeline : has 
+    timeline_event {
+        integer id PK
+        integer widget_id FK "The id from widget"
+        integer engagement_id FK "The id from engagement"
+        integer timeline_id FK "The id from timeline"
+        string description
+        string time
+        enum status
+        integer position
+        timestamp created_date
+        timestamp updated_date        
+        string created_by
+        string updated_by
+    }
+    widget only one to zero or more timeline_event : has 
     widget_documents {
         integer id PK
         string title
diff --git a/met-api/migrations/versions/3e4dc76a96ab_added_the_timeline_widget.py b/met-api/migrations/versions/3e4dc76a96ab_added_the_timeline_widget.py
new file mode 100644
index 000000000..e2619da49
--- /dev/null
+++ b/met-api/migrations/versions/3e4dc76a96ab_added_the_timeline_widget.py
@@ -0,0 +1,44 @@
+"""Added the Timeline Widget.
+
+Revision ID: 3e4dc76a96ab
+Revises: 02ff8ecc6b91
+Create Date: 2023-12-05 17:04:46.304368
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+from met_api.constants.timeline_event_status import TimelineEventStatus
+
+# revision identifiers, used by Alembic.
+revision = '3e4dc76a96ab'
+down_revision = '02ff8ecc6b91'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.create_table('widget_timeline',
+    sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True, nullable=False),
+    sa.Column('engagement_id', sa.Integer(), nullable=False),
+    sa.Column('title', sa.String(length=255), nullable=True),
+    sa.Column('description', sa.Text(), nullable=True),
+    sa.ForeignKeyConstraint(['engagement_id'], ['engagement.id'], ondelete='CASCADE'),
+    sa.PrimaryKeyConstraint('id'),
+    )
+    op.create_table('timeline_event',
+    sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True, nullable=False),
+    sa.Column('engagement_id', sa.Integer(), nullable=False),
+    sa.Column('timeline_id', sa.Integer(), nullable=False),
+    sa.Column('status', sa.Enum(TimelineEventStatus), nullable=True),
+    sa.Column('position', sa.Integer(), nullable=True),
+    sa.Column('description', sa.Text(), nullable=True),
+    sa.Column('time', sa.String(length=255), nullable=True),
+    sa.ForeignKeyConstraint(['engagement_id'], ['engagement.id'], ondelete='CASCADE'),
+    sa.ForeignKeyConstraint(['timeline_id'], ['widget_timeline.id'], ondelete='CASCADE'),
+    sa.PrimaryKeyConstraint('id'),
+    )
+
+def downgrade():
+    op.drop_table('widget_timeline')
+    op.drop_table('timeline_event')
diff --git a/met-api/migrations/versions/4114001e1a4c_add_widget_id_to_widget_timeline_and_.py b/met-api/migrations/versions/4114001e1a4c_add_widget_id_to_widget_timeline_and_.py
new file mode 100644
index 000000000..a34d93048
--- /dev/null
+++ b/met-api/migrations/versions/4114001e1a4c_add_widget_id_to_widget_timeline_and_.py
@@ -0,0 +1,27 @@
+"""Add widget_id to widget_timeline and timeline_event tables
+
+Revision ID: 4114001e1a4c
+Revises: c09e77fde608
+Create Date: 2023-12-11 15:46:30.773046
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = '4114001e1a4c'
+down_revision = 'c09e77fde608'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.add_column('widget_timeline', sa.Column('widget_id', sa.Integer()))
+    op.create_foreign_key('timeline_widget_fk', 'widget_timeline', 'widget', ['widget_id'], ['id'], ondelete='CASCADE')
+    op.add_column('timeline_event', sa.Column('widget_id', sa.Integer()))
+    op.create_foreign_key('event_widget_fk', 'timeline_event', 'widget', ['widget_id'], ['id'], ondelete='CASCADE')
+
+def downgrade():
+    op.drop_column('widget_timeline', 'widget_id')
+    op.drop_column('timeline_event', 'widget_id')
diff --git a/met-api/migrations/versions/c09e77fde608_added_enum_value_for_timeline_widget.py b/met-api/migrations/versions/c09e77fde608_added_enum_value_for_timeline_widget.py
new file mode 100644
index 000000000..0d55ae41d
--- /dev/null
+++ b/met-api/migrations/versions/c09e77fde608_added_enum_value_for_timeline_widget.py
@@ -0,0 +1,61 @@
+"""Added enum value for Timeline Widget.
+
+Revision ID: c09e77fde608
+Revises: 3e4dc76a96ab
+Create Date: 2023-12-06 11:46:20.934373
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = 'c09e77fde608'
+down_revision = '3e4dc76a96ab'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.add_column('timeline_event', sa.Column('created_date', sa.DateTime(), nullable=False))
+    op.add_column('timeline_event', sa.Column('updated_date', sa.DateTime(), nullable=True))
+    op.add_column('timeline_event', sa.Column('created_by', sa.String(length=50), nullable=True))
+    op.add_column('timeline_event', sa.Column('updated_by', sa.String(length=50), nullable=True))
+    
+    op.add_column('widget_timeline', sa.Column('created_date', sa.DateTime(), nullable=False))
+    op.add_column('widget_timeline', sa.Column('updated_date', sa.DateTime(), nullable=True))
+    op.add_column('widget_timeline', sa.Column('created_by', sa.String(length=50), nullable=True))
+    op.add_column('widget_timeline', sa.Column('updated_by', sa.String(length=50), nullable=True))
+
+    widget_type_table = sa.table('widget_type',
+        sa.Column('id', sa.Integer),
+        sa.Column('name', sa.String),
+        sa.Column('description', sa.String))
+
+    op.bulk_insert(
+        widget_type_table,
+        [
+            {
+                'id': 8,
+                'name': 'CAC Form',
+                'description': 'Add a CAC Form to your project',
+            },
+            {
+                'id': 9,
+                'name': 'Timeline',
+                'description': 'Create a timeline for a series of events',
+            },
+        ]
+    )
+
+def downgrade():
+    op.drop_column('widget_timeline', 'updated_by')
+    op.drop_column('widget_timeline', 'created_by')
+    op.drop_column('widget_timeline', 'updated_date')
+    op.drop_column('widget_timeline', 'created_date')
+    op.drop_column('timeline_event', 'updated_by')
+    op.drop_column('timeline_event', 'created_by')
+    op.drop_column('timeline_event', 'updated_date')
+    op.drop_column('timeline_event', 'created_date')
+    op.delete(widget_type_table).filter_by(id=8)
+    op.delete(widget_type_table).filter_by(id=9)
diff --git a/met-api/src/met_api/config.py b/met-api/src/met_api/config.py
index 1c1d26e7d..b022ca549 100644
--- a/met-api/src/met_api/config.py
+++ b/met-api/src/met_api/config.py
@@ -36,7 +36,7 @@
 os.environ = {k: v for k, v in os.environ.items() if v}
 
 
-def get_named_config(environment: str | None) -> 'Config':
+def get_named_config(environment: 'str | None') -> 'Config':
     """
     Retrieve a configuration object by name. Used by the Flask app factory.
 
diff --git a/met-api/src/met_api/constants/timeline_event_status.py b/met-api/src/met_api/constants/timeline_event_status.py
new file mode 100644
index 000000000..20a52e107
--- /dev/null
+++ b/met-api/src/met_api/constants/timeline_event_status.py
@@ -0,0 +1,10 @@
+"""Constants of timeline events."""
+from enum import IntEnum
+
+
+class TimelineEventStatus(IntEnum):
+    """Enum of timeline event status status."""
+
+    Pending = 1
+    InProgress = 2
+    Completed = 3
\ No newline at end of file
diff --git a/met-api/src/met_api/models/__init__.py b/met-api/src/met_api/models/__init__.py
index 88ff3621f..b61d5a5af 100644
--- a/met-api/src/met_api/models/__init__.py
+++ b/met-api/src/met_api/models/__init__.py
@@ -49,3 +49,5 @@
 from .report_setting import ReportSetting
 from .widget_video import WidgetVideo
 from .cac_form import CACForm
+from .widget_timeline import WidgetTimeline
+from .timeline_event import TimelineEvent
diff --git a/met-api/src/met_api/models/timeline_event.py b/met-api/src/met_api/models/timeline_event.py
new file mode 100644
index 000000000..7a80a12dc
--- /dev/null
+++ b/met-api/src/met_api/models/timeline_event.py
@@ -0,0 +1,51 @@
+"""Timeline Event model class.
+
+Manages the timeline events
+"""
+from __future__ import annotations
+
+from sqlalchemy.sql.schema import ForeignKey
+from met_api.constants.timeline_event_status import TimelineEventStatus
+
+from .base_model import BaseModel
+from .db import db
+
+
+class TimelineEvent(BaseModel):
+    """Definition of the TimelineEvent entity."""
+
+    __tablename__ = 'timeline_event'
+    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
+    engagement_id = db.Column(db.Integer, ForeignKey('engagement.id', ondelete='CASCADE'), nullable=True)
+    widget_id = db.Column(db.Integer, ForeignKey('widget.id', ondelete='CASCADE'), nullable=True)
+    timeline_id = db.Column(db.Integer, ForeignKey('widget_timeline.id', ondelete='CASCADE'), nullable=True)
+    status = db.Column(db.Enum(TimelineEventStatus), nullable=False)
+    position = db.Column(db.Integer, nullable=False)
+    description = db.Column(db.Text(), nullable=True)
+    time = db.Column(db.String(255), nullable=True)
+
+    @classmethod
+    def delete_event(cls, timeline_id):
+        """Delete timeline."""
+        timeline_event = db.session.query(TimelineEvent) \
+            .filter(TimelineEvent.timeline_id == timeline_id)
+        timeline_event.delete()
+        db.session.commit()
+
+    @classmethod
+    def get_timeline_events(cls, timeline_id) -> list[TimelineEvent]:
+        """Get timeline event."""
+        timeline_event = db.session.query(TimelineEvent) \
+            .filter(TimelineEvent.timeline_id == timeline_id) \
+            .all()
+        return timeline_event
+
+    @classmethod
+    def update_timeline_event(cls, timeline_id, event_data: dict) -> TimelineEvent:
+        """Update timeline event."""
+        timeline_event: TimelineEvent = TimelineEvent.query.get(timeline_id)
+        if timeline_event:
+            for key, value in event_data.items():
+                setattr(timeline_event, key, value)
+            timeline_event.save()
+        return timeline_event
diff --git a/met-api/src/met_api/models/widget_timeline.py b/met-api/src/met_api/models/widget_timeline.py
new file mode 100755
index 000000000..68a007793
--- /dev/null
+++ b/met-api/src/met_api/models/widget_timeline.py
@@ -0,0 +1,45 @@
+"""WidgetTimeline model class.
+
+Manages the timeline widget
+"""
+from __future__ import annotations
+from typing import Optional
+from sqlalchemy.sql.schema import ForeignKey
+from met_api.models.timeline_event import TimelineEvent
+from met_api.services.timeline_event_service import TimelineEventService
+from .base_model import BaseModel
+from .db import db
+
+class WidgetTimeline(BaseModel):  # pylint: disable=too-few-public-methods, too-many-instance-attributes
+    """Definition of the Timeline entity."""
+
+    __tablename__ = 'widget_timeline'
+    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
+    engagement_id = db.Column(db.Integer, ForeignKey('engagement.id', ondelete='CASCADE'), nullable=True)
+    widget_id = db.Column(db.Integer, ForeignKey('widget.id', ondelete='CASCADE'), nullable=True)
+    title = db.Column(db.String(255), nullable=True)
+    description = db.Column(db.Text(), nullable=True)
+
+    # Relationship to timeline_event
+    events = db.relationship(TimelineEvent, backref='widget_timeline', lazy=True)
+
+    @classmethod
+    def get_timeline(cls, timeline_id) -> list[WidgetTimeline]:
+        """Get timeline."""
+        widget_timeline = db.session.query(WidgetTimeline) \
+            .filter(WidgetTimeline.widget_id == timeline_id) \
+            .all()
+        return widget_timeline
+
+    @classmethod
+    def update_timeline(cls, timeline_id, timeline_data: dict) -> Optional[WidgetTimeline or None]:
+        """Update timeline."""
+        TimelineEvent.delete_event(timeline_id)
+        widget_timeline: WidgetTimeline = WidgetTimeline.query.get(timeline_id)
+        if widget_timeline:
+            widget_timeline.title = timeline_data.get('title')
+            widget_timeline.description = timeline_data.get('description')
+            for event in timeline_data.get('events', []):
+                TimelineEventService.create_timeline_event(timeline_id, event)
+            widget_timeline.save()
+        return widget_timeline
diff --git a/met-api/src/met_api/resources/__init__.py b/met-api/src/met_api/resources/__init__.py
index 2451f6c11..57db720bc 100644
--- a/met-api/src/met_api/resources/__init__.py
+++ b/met-api/src/met_api/resources/__init__.py
@@ -49,6 +49,7 @@
 from .widget_video import API as WIDGET_VIDEO_API
 from .engagement_settings import API as ENGAGEMENT_SETTINGS_API
 from .cac_form import API as CAC_FORM_API
+from .widget_timeline import API as WIDGET_TIMELINE_API
 
 __all__ = ('API_BLUEPRINT',)
 
@@ -89,3 +90,4 @@
 API.add_namespace(WIDGET_VIDEO_API, path='/widgets/<int:widget_id>/videos')
 API.add_namespace(ENGAGEMENT_SETTINGS_API)
 API.add_namespace(CAC_FORM_API, path='/engagements/<int:engagement_id>/cacform')
+API.add_namespace(WIDGET_TIMELINE_API, path='/widgets/<int:widget_id>/timelines')
diff --git a/met-api/src/met_api/resources/widget_timeline.py b/met-api/src/met_api/resources/widget_timeline.py
new file mode 100644
index 000000000..19344a4c5
--- /dev/null
+++ b/met-api/src/met_api/resources/widget_timeline.py
@@ -0,0 +1,79 @@
+# Copyright © 2021 Province of British Columbia
+#
+# Licensed under the Apache License, Version 2.0 (the 'License');
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an 'AS IS' BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""API endpoints for managing a timeline widget resource."""
+from http import HTTPStatus
+
+from flask import jsonify, request
+from flask_cors import cross_origin
+from flask_restx import Namespace, Resource
+
+from met_api.auth import jwt as _jwt
+from met_api.exceptions.business_exception import BusinessException
+from met_api.schemas import utils as schema_utils
+from met_api.schemas.widget_timeline import WidgetTimelineSchema
+from met_api.services.widget_timeline_service import WidgetTimelineService
+from met_api.utils.util import allowedorigins, cors_preflight
+
+
+API = Namespace('widget_timelines', description='Endpoints for Timeline Widget Management')
+"""Widget Timelines"""
+
+
+@cors_preflight('GET, POST')
+@API.route('')
+class Timelines(Resource):
+    """Resource for managing timeline widgets."""
+
+    @staticmethod
+    @cross_origin(origins=allowedorigins())
+    def get(widget_id):
+        """Get timeline widget."""
+        try:
+            widget_timeline = WidgetTimelineService().get_timeline(widget_id)
+            return jsonify(WidgetTimelineSchema().dump(widget_timeline, many=True)), HTTPStatus.OK
+        except BusinessException as err:
+            return str(err), err.status_code
+
+    @staticmethod
+    @cross_origin(origins=allowedorigins())
+    @_jwt.requires_auth
+    def post(widget_id):
+        """Create timeline widget."""
+        try:
+            request_json = request.get_json()
+            widget_timeline = WidgetTimelineService().create_timeline(widget_id, request_json)
+            return WidgetTimelineSchema().dump(widget_timeline), HTTPStatus.OK
+        except BusinessException as err:
+            return str(err), err.status_code
+
+
+@cors_preflight('PATCH')
+@API.route('/<int:timeline_widget_id>')
+class Timeline(Resource):
+    """Resource for managing timeline widgets."""
+
+    @staticmethod
+    @cross_origin(origins=allowedorigins())
+    @_jwt.requires_auth
+    def patch(widget_id, timeline_widget_id):
+        """Update timeline widget."""
+        request_json = request.get_json()
+        valid_format, errors = schema_utils.validate(request_json, 'timeline_widget_update')
+        if not valid_format:
+            return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST
+        try:
+            widget_timeline = WidgetTimelineService().update_timeline(widget_id, timeline_widget_id, request_json)
+            return WidgetTimelineSchema().dump(widget_timeline), HTTPStatus.OK
+        except BusinessException as err:
+            return str(err), err.status_code
diff --git a/met-api/src/met_api/schemas/schemas/timeline_widget_update.json b/met-api/src/met_api/schemas/schemas/timeline_widget_update.json
new file mode 100644
index 000000000..3cad6755e
--- /dev/null
+++ b/met-api/src/met_api/schemas/schemas/timeline_widget_update.json
@@ -0,0 +1,79 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema",
+  "$id": "https://met.gov.bc.ca/.well_known/schemas/timeline_widget_update",
+  "type": "object",
+  "title": "The root schema",
+  "description": "The root schema comprises the entire JSON document.",
+  "default": {},
+  "examples": [
+    {
+      "title": "A timeline widget title",
+      "description": "A timeline widget description",
+      "events": [
+        {
+          "id": 3,
+          "engagement_id": 4,
+          "timeline_id": 9,
+          "widget_id": 111,
+          "description": "An event description",
+          "time": "An event time",
+          "position": 0,
+          "status": 1
+        },
+        {
+          "id": 4,
+          "engagement_id": 4,
+          "timeline_id": 9,
+          "widget_id": 111,
+          "description": "An event description",
+          "time": "An event time",
+          "position": 1,
+          "status": 2
+        }
+      ]
+    }
+  ],
+  "required": [],
+  "properties": {
+    "title": {
+      "$id": "#/properties/title",
+      "type": "string",
+      "title": "Timeline title",
+      "description": "The title of this timeline.",
+      "examples": ["A timeline widget title"]
+    },
+    "description": {
+      "$id": "#/properties/description",
+      "type": "string",
+      "title": "Timeline description",
+      "description": "The description of this timeline.",
+      "examples": ["A timeline widget description"]
+    },
+    "events": {
+      "$id": "#/properties/events",
+      "type": "array",
+      "title": "Timeline events",
+      "description": "The events of this timeline.",
+      "examples": [{
+        "id": 3,
+        "engagement_id": 4,
+        "timeline_id": 9,
+        "widget_id": 111,
+        "description": "An event description",
+        "time": "An event time",
+        "position": 0,
+        "status": 1
+      },
+      {
+        "id": 4,
+        "engagement_id": 4,
+        "timeline_id": 9,
+        "widget_id": 111,
+        "description": "An event description",
+        "time": "An event time",
+        "position": 1,
+        "status": 2
+      }]
+    }
+  }
+}
diff --git a/met-api/src/met_api/schemas/widget_timeline.py b/met-api/src/met_api/schemas/widget_timeline.py
new file mode 100644
index 000000000..52ae8f133
--- /dev/null
+++ b/met-api/src/met_api/schemas/widget_timeline.py
@@ -0,0 +1,39 @@
+# Copyright © 2019 Province of British Columbia
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Manager for widget timeline schema."""
+
+from met_api.models.widget_timeline import WidgetTimeline as WidgetTimelineModel
+from met_api.models.timeline_event import TimelineEvent as TimelineEventModel
+
+from marshmallow import Schema
+from marshmallow_sqlalchemy.fields import Nested
+
+class TimelineEventSchema(Schema):  # pylint: disable=too-many-ancestors, too-few-public-methods
+
+    class Meta:  # pylint: disable=too-few-public-methods
+        """All of the fields in the Timeline Event schema."""
+
+        model = TimelineEventModel
+        fields = ('id', 'engagement_id', 'widget_id', 'timeline_id', 'description', 'time', 'position', 'status')
+
+
+class WidgetTimelineSchema(Schema):  # pylint: disable=too-many-ancestors, too-few-public-methods
+
+    class Meta:  # pylint: disable=too-few-public-methods
+        """All of the fields in the Widget Timeline schema."""
+
+        model = WidgetTimelineModel
+        fields = ('id', 'engagement_id', 'widget_id', 'title', 'description', 'events')
+    
+    events = Nested(TimelineEventSchema, many=True)
diff --git a/met-api/src/met_api/services/timeline_event_service.py b/met-api/src/met_api/services/timeline_event_service.py
new file mode 100644
index 000000000..1f9cf87a5
--- /dev/null
+++ b/met-api/src/met_api/services/timeline_event_service.py
@@ -0,0 +1,55 @@
+"""Service for Widget Timeline management."""
+from met_api.constants.membership_type import MembershipType
+from met_api.models.widget_timeline import TimelineEvent as TimelineEventModel
+from met_api.services import authorization
+from met_api.utils.roles import Role
+
+
+class TimelineEventService:
+    """Timeline event management service."""
+
+    @staticmethod
+    def get_timeline_event(timeline_id):
+        """Get timeline event by timeline id."""
+        timeline_event = TimelineEventModel.get_event(timeline_id)
+        return timeline_event
+
+    @staticmethod
+    def create_timeline_event(timeline_id, event_details: dict):
+        """Create timeline event for a timeline."""
+        event_data = dict(event_details)
+        eng_id = event_data.get('engagement_id')
+        authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name,
+                                               Role.EDIT_ENGAGEMENT.value), engagement_id=eng_id)
+
+        timeline_event = TimelineEventService._create_timeline_event_model(timeline_id, event_data)
+        timeline_event.commit()
+        return timeline_event
+
+    @staticmethod
+    def update_timeline_event(timeline_id, event_id, event_data):
+        """Update timeline event."""
+        timeline_event: TimelineEventModel = TimelineEventModel.find_by_id(event_id)
+        authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name,
+                                               Role.EDIT_ENGAGEMENT.value), engagement_id=timeline_event.engagement_id)
+
+        if not timeline_event:
+            raise KeyError('Event not found')
+
+        if timeline_event.timeline_id != timeline_id:
+            raise ValueError('Invalid timeline and event')
+
+        return TimelineEventModel.update_timeline_event(timeline_event.id, event_data)
+
+    @staticmethod
+    def _create_timeline_event_model(timeline_id, event_data: dict):
+        timeline_event_model: TimelineEventModel = TimelineEventModel()
+        timeline_event_model.widget_id = event_data.get('widget_id')
+        timeline_event_model.engagement_id = event_data.get('engagement_id')
+        timeline_event_model.timeline_id = timeline_id
+        timeline_event_model.status = event_data.get('status')
+        timeline_event_model.position = event_data.get('position')
+        timeline_event_model.description = event_data.get('description')
+        timeline_event_model.time = event_data.get('time')
+        timeline_event_model.flush()
+        return timeline_event_model
diff --git a/met-api/src/met_api/services/widget_timeline_service.py b/met-api/src/met_api/services/widget_timeline_service.py
new file mode 100644
index 000000000..5cd436012
--- /dev/null
+++ b/met-api/src/met_api/services/widget_timeline_service.py
@@ -0,0 +1,69 @@
+"""Service for Widget Timeline management."""
+from met_api.constants.membership_type import MembershipType
+from met_api.models.widget_timeline import WidgetTimeline as WidgetTimelineModel
+from met_api.models.timeline_event import TimelineEvent as TimelineEventModel
+from met_api.services import authorization
+from met_api.utils.roles import Role
+
+
+class WidgetTimelineService:
+    """Widget Timeline management service."""
+
+    @staticmethod
+    def get_timeline(widget_id: int):
+        """Get timeline by widget id."""
+        widget_timeline = WidgetTimelineModel.get_timeline(widget_id)
+        return widget_timeline
+
+    @staticmethod
+    def create_timeline(widget_id: int, timeline_details: dict):
+        """Create timeline for the widget."""
+        timeline_data = dict(timeline_details)
+        eng_id = timeline_data.get('engagement_id')
+        authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name,
+                                               Role.EDIT_ENGAGEMENT.value), engagement_id=eng_id)
+
+        widget_timeline = WidgetTimelineService._create_timeline_model(widget_id, timeline_data)
+        widget_timeline.commit()
+        return widget_timeline
+
+    @staticmethod
+    def update_timeline(widget_id: int, timeline_id: int, timeline_data: dict):
+        """Update timeline widget."""
+        events = timeline_data.get("events")
+        first_event = events[0]
+        widget_timeline: WidgetTimelineModel = WidgetTimelineModel.find_by_id(timeline_id)
+        authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name,
+                                               Role.EDIT_ENGAGEMENT.value), engagement_id=first_event.get('engagement_id'))
+        if not widget_timeline:
+            raise KeyError('Timeline widget not found')
+
+        if widget_timeline.widget_id != widget_id:
+            raise ValueError('Invalid widget ID')
+
+        if widget_timeline.id != timeline_id:
+            raise ValueError('Invalid timeline ID')
+
+        return WidgetTimelineModel.update_timeline(timeline_id, timeline_data)
+
+    @staticmethod
+    def _create_timeline_model(widget_id: int, timeline_data: dict):
+        timeline_model: WidgetTimelineModel = WidgetTimelineModel()
+        timeline_model.widget_id = widget_id
+        timeline_model.engagement_id = timeline_data.get('engagement_id')
+        timeline_model.title = timeline_data.get('title')
+        timeline_model.description = timeline_data.get('description')
+        for event in timeline_data.get('events', []):
+            timeline_model.events.append(
+                TimelineEventModel(
+                    widget_id = widget_id,
+                    engagement_id = event.get('engagement_id'),
+                    timeline_id = event.get('timeline_id'),
+                    description = event.get('description'),
+                    time = event.get('time'),
+                    position = event.get('position'),
+                    status = event.get('status'),
+                )
+            )
+        timeline_model.flush()
+        return timeline_model
diff --git a/met-cron/config.py b/met-cron/config.py
index 47cdda6bb..070319461 100644
--- a/met-cron/config.py
+++ b/met-cron/config.py
@@ -37,7 +37,7 @@
 }
 
 
-def get_named_config(environment: str | None) -> '_Config':
+def get_named_config(environment: 'str | None') -> '_Config':
     """
     Retrieve a configuration object by name. Used by the Flask app factory.
 
diff --git a/met-web/src/apiManager/endpoints/index.ts b/met-web/src/apiManager/endpoints/index.ts
index 366337c76..a228feb0b 100644
--- a/met-web/src/apiManager/endpoints/index.ts
+++ b/met-web/src/apiManager/endpoints/index.ts
@@ -134,6 +134,11 @@ const Endpoints = {
         CREATE: `${AppConfig.apiUrl}/widgets/widget_id/videos`,
         UPDATE: `${AppConfig.apiUrl}/widgets/widget_id/videos/video_widget_id`,
     },
+    TimelineWidgets: {
+        GET: `${AppConfig.apiUrl}/widgets/widget_id/timelines`,
+        CREATE: `${AppConfig.apiUrl}/widgets/widget_id/timelines`,
+        UPDATE: `${AppConfig.apiUrl}/widgets/widget_id/timelines/timeline_id`,
+    },
     Tenants: {
         GET: `${AppConfig.apiUrl}/tenants/tenant_id`,
     },
diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Timeline/Form.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Timeline/Form.tsx
new file mode 100644
index 000000000..fdbf8016d
--- /dev/null
+++ b/met-web/src/components/engagement/form/EngagementWidgets/Timeline/Form.tsx
@@ -0,0 +1,381 @@
+import React, { useContext, useEffect } from 'react';
+import Divider from '@mui/material/Divider';
+import { Grid, MenuItem, TextField, Select, SelectChangeEvent } from '@mui/material';
+import { MetDescription, MetLabel, MidScreenLoader, PrimaryButton, SecondaryButton } from 'components/common';
+import { SubmitHandler } from 'react-hook-form';
+import { useAppDispatch } from 'hooks';
+import { openNotification } from 'services/notificationService/notificationSlice';
+import { WidgetDrawerContext } from '../WidgetDrawerContext';
+import { TimelineContext } from './TimelineContext';
+import { patchTimeline, postTimeline } from 'services/widgetService/TimelineService';
+import { WidgetTitle } from '../WidgetTitle';
+import { TimelineEvent } from 'models/timelineWidget';
+
+interface DetailsForm {
+    title: string;
+    description: string;
+    events: TimelineEvent[];
+}
+
+interface WidgetState {
+    title: string;
+    description: string;
+}
+
+const Form = () => {
+    const dispatch = useAppDispatch();
+    const { widget, isLoadingTimelineWidget, timelineWidget } = useContext(TimelineContext);
+    const { handleWidgetDrawerOpen } = useContext(WidgetDrawerContext);
+    const [isCreating, setIsCreating] = React.useState(false);
+
+    const newEvent: TimelineEvent = {
+        id: 0,
+        widget_id: widget?.id || 0,
+        timeline_id: 0,
+        engagement_id: widget?.engagement_id || 0,
+        description: '',
+        time: '',
+        status: 1,
+        position: 0,
+    };
+
+    const [timelineEvents, setTimelineEvents] = React.useState<TimelineEvent[]>(
+        timelineWidget ? timelineWidget.events.sort((a, b) => a.position - b.position) : [newEvent],
+    );
+    const [timelineWidgetState, setTimelineWidgetState] = React.useState<WidgetState>({
+        description: timelineWidget?.description || '',
+        title: timelineWidget?.title || '',
+    });
+
+    const timelineEventStatusTypes = [
+        {
+            title: 'Pending',
+            value: 1,
+        },
+        {
+            title: 'In Progress',
+            value: 2,
+        },
+        {
+            title: 'Completed',
+            value: 3,
+        },
+    ];
+
+    useEffect(() => {
+        if (timelineWidget) {
+            setTimelineEvents(timelineWidget.events.sort((a, b) => a.position - b.position));
+            setTimelineWidgetState(timelineWidget);
+        }
+    }, [timelineWidget]);
+
+    const createTimeline = async (data: DetailsForm) => {
+        if (!widget) {
+            return;
+        }
+
+        const { title, description, events } = data;
+        await postTimeline(widget.id, {
+            widget_id: widget.id,
+            engagement_id: widget.engagement_id,
+            title: title,
+            description: description,
+            events: events,
+        });
+        dispatch(openNotification({ severity: 'success', text: 'A new timeline was successfully added' }));
+    };
+
+    const updateTimeline = async (data: DetailsForm) => {
+        if (!widget || !timelineWidget) {
+            return;
+        }
+
+        if (Object.keys(data).length === 0) {
+            return;
+        }
+
+        await patchTimeline(widget.id, timelineWidget.id, { ...data });
+        dispatch(openNotification({ severity: 'success', text: 'The timeline widget was successfully updated' }));
+    };
+
+    const saveTimelineWidget = (data: DetailsForm) => {
+        if (!timelineWidget) {
+            return createTimeline(data);
+        }
+        return updateTimeline(data);
+    };
+
+    const onSubmit: SubmitHandler<DetailsForm> = async (data: DetailsForm) => {
+        if (!widget) {
+            return;
+        }
+        try {
+            setIsCreating(true);
+            await saveTimelineWidget(data);
+            setIsCreating(false);
+            handleWidgetDrawerOpen(false);
+        } catch (error) {
+            dispatch(openNotification({ severity: 'error', text: 'An error occurred while trying to add the event' }));
+            setIsCreating(false);
+        }
+    };
+
+    const handleOnSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+        event.preventDefault();
+        const eventsForSubmission = [...timelineEvents];
+        eventsForSubmission.forEach((event, index) => {
+            event.position = index;
+        });
+        eventsForSubmission.sort((a, b) => a.position - b.position);
+        /* eslint "no-warning-comments": [1, { "terms": ["todo", "fix me, replace any type"] }] */
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        const eventTarget = event.target as any;
+        const restructuredData = {
+            title: eventTarget['title']?.value,
+            description: eventTarget['description']?.value,
+            events: eventsForSubmission,
+        };
+        setTimelineEvents(eventsForSubmission);
+        onSubmit(restructuredData);
+    };
+
+    const handleAddEvent = () => {
+        if (!timelineEvents) {
+            return;
+        }
+        const newEventWithCorrectIndex = newEvent;
+        newEventWithCorrectIndex.position = timelineEvents.length;
+        setTimelineEvents([...timelineEvents, newEventWithCorrectIndex]);
+    };
+
+    const handleRemoveEvent = (event: React.ChangeEvent<HTMLInputElement>) => {
+        if (!timelineEvents) {
+            return;
+        }
+        const position = Number(event.target.value);
+        const dataToSplice: TimelineEvent[] = [...timelineEvents];
+        dataToSplice.splice(position, 1);
+        dataToSplice.forEach((event, index) => {
+            event.position = index;
+        });
+        setTimelineEvents([...dataToSplice]);
+    };
+
+    const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>, property: string) => {
+        if (!timelineEvents) {
+            return;
+        }
+        const newValue = e.currentTarget.value;
+        if ('description' === property) {
+            setTimelineWidgetState({ ...timelineWidgetState, description: newValue });
+        } else if ('title' === property) {
+            setTimelineWidgetState({ ...timelineWidgetState, title: newValue });
+        }
+    };
+
+    const handleEventTextChange = (e: React.ChangeEvent<HTMLInputElement>, index: number, property: string) => {
+        if (!timelineEvents) {
+            return;
+        }
+        const newValue = e.currentTarget.value;
+        const newArray = [...timelineEvents];
+        if ('description' === property) {
+            newArray[index].description = newValue;
+            setTimelineEvents([...newArray]);
+        } else if ('time' === property) {
+            newArray[index].time = newValue;
+            setTimelineEvents([...newArray]);
+        }
+    };
+
+    const handleSelectChange = (e: SelectChangeEvent<string>, index: number) => {
+        if (!timelineEvents) {
+            return;
+        }
+        const newValue = e.target.value;
+        const newArray = [...timelineEvents];
+        newArray[index].status = Number(newValue);
+        setTimelineEvents([...newArray]);
+    };
+
+    if (isLoadingTimelineWidget || !widget) {
+        return (
+            <Grid container direction="row" alignItems={'flex-start'} justifyContent="flex-start" spacing={2}>
+                <Grid item xs={12}>
+                    <MidScreenLoader />
+                </Grid>
+            </Grid>
+        );
+    }
+
+    return (
+        <Grid item xs={12} container alignItems="flex-start" justifyContent={'flex-start'} spacing={3}>
+            <Grid item xs={12}>
+                <WidgetTitle widget={widget} />
+                <Divider sx={{ marginTop: '0.5em' }} />
+            </Grid>
+            <Grid item xs={12}>
+                <form onSubmit={(event) => handleOnSubmit(event)} id="timelineForm">
+                    <Grid container direction="row" alignItems={'flex-start'} justifyContent="flex-start" spacing={2}>
+                        <Grid item xs={12}>
+                            <MetLabel>Title</MetLabel>
+                            <MetDescription>The title must be less than 255 characters.</MetDescription>
+                            <TextField
+                                name="title"
+                                variant="outlined"
+                                label=" "
+                                InputLabelProps={{
+                                    shrink: false,
+                                }}
+                                fullWidth
+                                value={timelineWidgetState?.title}
+                                onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
+                                    handleTextChange(event, 'title');
+                                }}
+                            />
+                        </Grid>
+                        <Grid item xs={12}>
+                            <MetLabel>Description</MetLabel>
+                            <TextField
+                                name="description"
+                                variant="outlined"
+                                label=" "
+                                InputLabelProps={{
+                                    shrink: false,
+                                }}
+                                fullWidth
+                                multiline
+                                rows={4}
+                                value={timelineWidgetState?.description}
+                                onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
+                                    handleTextChange(event, 'description');
+                                }}
+                            />
+                        </Grid>
+                        <Grid
+                            item
+                            xs={12}
+                            container
+                            direction="column"
+                            alignItems={'stretch'}
+                            justifyContent="flex-start"
+                            spacing={2}
+                            mt={'3em'}
+                        >
+                            {timelineEvents &&
+                                timelineEvents.map((tEvent, index) => (
+                                    <Grid
+                                        item
+                                        className={'event' + (index + 1)}
+                                        key={'event' + index + 1}
+                                        spacing={1}
+                                        container
+                                        mb={'1em'}
+                                        xs={12}
+                                    >
+                                        <MetLabel sx={{ paddingLeft: '8px' }}>{'EVENT ' + (index + 1)}</MetLabel>
+
+                                        <Grid item xs={12}>
+                                            <MetLabel>Event Description</MetLabel>
+                                            <MetDescription>Describe the timeline event.</MetDescription>
+                                            <TextField
+                                                name={'eventDescription' + (index + 1)}
+                                                id={'eventDescription' + (index + 1)}
+                                                variant="outlined"
+                                                value={tEvent.description}
+                                                fullWidth
+                                                onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
+                                                    handleEventTextChange(event, index, 'description');
+                                                }}
+                                            />
+                                        </Grid>
+
+                                        <Grid item xs={12}>
+                                            <MetLabel>Event Time</MetLabel>
+                                            <MetDescription>When did the event happen?</MetDescription>
+                                            <TextField
+                                                name={'eventTime' + (index + 1)}
+                                                id={'eventTime' + (index + 1)}
+                                                variant="outlined"
+                                                value={tEvent.time}
+                                                fullWidth
+                                                onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
+                                                    handleEventTextChange(event, index, 'time');
+                                                }}
+                                            />
+                                        </Grid>
+
+                                        <Grid item xs={12}>
+                                            <MetLabel>Event Status</MetLabel>
+                                            <Select
+                                                name={'eventStatus' + (index + 1)}
+                                                id={'eventStatus' + (index + 1)}
+                                                variant="outlined"
+                                                value={tEvent.status?.toString()}
+                                                defaultValue="Select an event status"
+                                                fullWidth
+                                                onChange={(event: SelectChangeEvent<string>) => {
+                                                    handleSelectChange(event, index);
+                                                }}
+                                            >
+                                                {timelineEventStatusTypes.map((statusType) => (
+                                                    <MenuItem
+                                                        key={`status-type-${statusType.value || 1}`}
+                                                        value={statusType.value || 1}
+                                                    >
+                                                        {statusType.title || 'Pending'}
+                                                    </MenuItem>
+                                                ))}
+                                            </Select>
+                                        </Grid>
+
+                                        {1 < timelineEvents.length && (
+                                            <Grid item xs={12} sx={{ marginTop: '8px' }}>
+                                                <SecondaryButton
+                                                    value={index}
+                                                    onClick={(event: React.ChangeEvent<HTMLInputElement>) => {
+                                                        handleRemoveEvent(event);
+                                                    }}
+                                                >
+                                                    Remove Event
+                                                </SecondaryButton>
+                                            </Grid>
+                                        )}
+
+                                        <Grid item xs={12}>
+                                            <Divider sx={{ marginTop: '1em' }} />
+                                        </Grid>
+                                    </Grid>
+                                ))}
+                            <Grid item>
+                                <PrimaryButton onClick={() => handleAddEvent()}>Add Event</PrimaryButton>
+                            </Grid>
+                        </Grid>
+
+                        <Grid
+                            item
+                            xs={12}
+                            container
+                            direction="row"
+                            alignItems={'flex-start'}
+                            justifyContent="flex-start"
+                            spacing={2}
+                            mt={'3em'}
+                        >
+                            <Grid item>
+                                <PrimaryButton type="submit" disabled={isCreating}>
+                                    Save & Close
+                                </PrimaryButton>
+                            </Grid>
+                            <Grid item>
+                                <SecondaryButton onClick={() => handleWidgetDrawerOpen(false)}>Cancel</SecondaryButton>
+                            </Grid>
+                        </Grid>
+                    </Grid>
+                </form>
+            </Grid>
+        </Grid>
+    );
+};
+
+export default Form;
diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Timeline/TimelineContext.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Timeline/TimelineContext.tsx
new file mode 100644
index 000000000..0292bb3a2
--- /dev/null
+++ b/met-web/src/components/engagement/form/EngagementWidgets/Timeline/TimelineContext.tsx
@@ -0,0 +1,63 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import { Widget, WidgetType } from 'models/widget';
+import { WidgetDrawerContext } from '../WidgetDrawerContext';
+import { useAppDispatch } from 'hooks';
+import { fetchTimelineWidgets } from 'services/widgetService/TimelineService/index';
+import { TimelineWidget } from 'models/timelineWidget';
+import { openNotification } from 'services/notificationService/notificationSlice';
+
+export interface TimelineContextProps {
+    widget: Widget | null;
+    isLoadingTimelineWidget: boolean;
+    timelineWidget: TimelineWidget | null;
+}
+
+export type EngagementParams = {
+    engagementId: string;
+};
+
+export const TimelineContext = createContext<TimelineContextProps>({
+    widget: null,
+    isLoadingTimelineWidget: true,
+    timelineWidget: null,
+});
+
+export const TimelineContextProvider = ({ children }: { children: JSX.Element | JSX.Element[] }) => {
+    const { widgets } = useContext(WidgetDrawerContext);
+    const dispatch = useAppDispatch();
+    const widget = widgets.find((widget) => widget.widget_type_id === WidgetType.Timeline) ?? null;
+    const [isLoadingTimelineWidget, setIsLoadingTimelineWidget] = useState(true);
+    const [timelineWidget, setTimelineWidget] = useState<TimelineWidget | null>(null);
+
+    const loadTimelineWidget = async () => {
+        if (!widget) {
+            return;
+        }
+        try {
+            const result = await fetchTimelineWidgets(widget.id);
+            setTimelineWidget(result[result.length - 1]);
+            setIsLoadingTimelineWidget(false);
+        } catch (error) {
+            dispatch(
+                openNotification({ severity: 'error', text: 'An error occurred while trying to load timeline data' }),
+            );
+            setIsLoadingTimelineWidget(false);
+        }
+    };
+
+    useEffect(() => {
+        loadTimelineWidget();
+    }, [widget]);
+
+    return (
+        <TimelineContext.Provider
+            value={{
+                widget,
+                isLoadingTimelineWidget,
+                timelineWidget,
+            }}
+        >
+            {children}
+        </TimelineContext.Provider>
+    );
+};
diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Timeline/TimelineOptionCard.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Timeline/TimelineOptionCard.tsx
new file mode 100644
index 000000000..1a306e696
--- /dev/null
+++ b/met-web/src/components/engagement/form/EngagementWidgets/Timeline/TimelineOptionCard.tsx
@@ -0,0 +1,101 @@
+import React, { useContext, useState } from 'react';
+import { MetPaper, MetLabel, MetDescription } from 'components/common';
+import { Grid, CircularProgress } from '@mui/material';
+import { WidgetDrawerContext } from '../WidgetDrawerContext';
+import { WidgetType } from 'models/widget';
+import { Else, If, Then } from 'react-if';
+import { ActionContext } from '../../ActionContext';
+import { useAppDispatch } from 'hooks';
+import { openNotification } from 'services/notificationService/notificationSlice';
+import { optionCardStyle } from '../constants';
+import { WidgetTabValues } from '../type';
+import { useCreateWidgetMutation } from 'apiManager/apiSlices/widgets';
+import AccessTimeIcon from '@mui/icons-material/AccessTime';
+
+const Title = 'Timeline';
+const TimelineOptionCard = () => {
+    const { widgets, loadWidgets, handleWidgetDrawerOpen, handleWidgetDrawerTabValueChange } =
+        useContext(WidgetDrawerContext);
+    const { savedEngagement } = useContext(ActionContext);
+    const dispatch = useAppDispatch();
+    const [createWidget] = useCreateWidgetMutation();
+    const [isCreatingWidget, setIsCreatingWidget] = useState(false);
+
+    const handleCreateWidget = async () => {
+        const alreadyExists = widgets.some((widget) => widget.widget_type_id === WidgetType.Timeline);
+        if (alreadyExists) {
+            handleWidgetDrawerTabValueChange(WidgetTabValues.TIMELINE_FORM);
+            return;
+        }
+
+        try {
+            setIsCreatingWidget(true);
+            await createWidget({
+                widget_type_id: WidgetType.Timeline,
+                engagement_id: savedEngagement.id,
+                title: Title,
+            }).unwrap();
+            await loadWidgets();
+            dispatch(
+                openNotification({
+                    severity: 'success',
+                    text: 'Timeline widget successfully created.',
+                }),
+            );
+            setIsCreatingWidget(false);
+            handleWidgetDrawerTabValueChange(WidgetTabValues.TIMELINE_FORM);
+        } catch (error) {
+            setIsCreatingWidget(false);
+            dispatch(openNotification({ severity: 'error', text: 'Error occurred while creating timeline widget' }));
+            handleWidgetDrawerOpen(false);
+        }
+    };
+
+    return (
+        <MetPaper
+            data-testid={`widget-drawer-option/${WidgetType.Timeline}`}
+            elevation={1}
+            sx={optionCardStyle}
+            onClick={() => handleCreateWidget()}
+        >
+            <If condition={isCreatingWidget}>
+                <Then>
+                    <Grid container alignItems="center" justifyContent="center" direction="row" height="5.5em">
+                        <CircularProgress color="inherit" />
+                    </Grid>
+                </Then>
+                <Else>
+                    <Grid
+                        container
+                        alignItems="flex-start"
+                        justifyContent="flex-start"
+                        direction="row"
+                        columnSpacing={1}
+                    >
+                        <Grid item>
+                            <AccessTimeIcon color="info" sx={{ p: 0.5, fontSize: '4em' }} />
+                        </Grid>
+                        <Grid
+                            container
+                            item
+                            alignItems="center"
+                            justifyContent="center"
+                            direction="row"
+                            rowSpacing={1}
+                            xs={8}
+                        >
+                            <Grid item xs={12}>
+                                <MetLabel>{Title}</MetLabel>
+                            </Grid>
+                            <Grid item xs={12}>
+                                <MetDescription>Add a timeline to this engagement</MetDescription>
+                            </Grid>
+                        </Grid>
+                    </Grid>
+                </Else>
+            </If>
+        </MetPaper>
+    );
+};
+
+export default TimelineOptionCard;
diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Timeline/index.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Timeline/index.tsx
new file mode 100644
index 000000000..fff9d4dda
--- /dev/null
+++ b/met-web/src/components/engagement/form/EngagementWidgets/Timeline/index.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import { TimelineContextProvider } from './TimelineContext';
+import Form from './Form';
+
+export const TimelineForm = () => {
+    return (
+        <TimelineContextProvider>
+            <Form />
+        </TimelineContextProvider>
+    );
+};
+
+export default TimelineForm;
diff --git a/met-web/src/components/engagement/form/EngagementWidgets/WidgetCardSwitch.tsx b/met-web/src/components/engagement/form/EngagementWidgets/WidgetCardSwitch.tsx
index 43f6af81f..29396bec7 100644
--- a/met-web/src/components/engagement/form/EngagementWidgets/WidgetCardSwitch.tsx
+++ b/met-web/src/components/engagement/form/EngagementWidgets/WidgetCardSwitch.tsx
@@ -107,6 +107,19 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps
                         }}
                     />
                 </Case>
+                <Case condition={widget.widget_type_id === WidgetType.Timeline}>
+                    <MetWidget
+                        testId={`event-${widget.widget_type_id}`}
+                        title={widget.title}
+                        onDelete={() => {
+                            removeWidget(widget.id);
+                        }}
+                        onEdit={() => {
+                            handleWidgetDrawerTabValueChange(WidgetTabValues.TIMELINE_FORM);
+                            handleWidgetDrawerOpen(true);
+                        }}
+                    />
+                </Case>
             </Switch>
         </>
     );
diff --git a/met-web/src/components/engagement/form/EngagementWidgets/WidgetDrawerTabs.tsx b/met-web/src/components/engagement/form/EngagementWidgets/WidgetDrawerTabs.tsx
index c8949658a..62e862efd 100644
--- a/met-web/src/components/engagement/form/EngagementWidgets/WidgetDrawerTabs.tsx
+++ b/met-web/src/components/engagement/form/EngagementWidgets/WidgetDrawerTabs.tsx
@@ -10,6 +10,7 @@ import Phases from './Phases';
 import EventsForm from './Events';
 import MapForm from './Map';
 import VideoForm from './Video';
+import TimelineForm from './Timeline';
 import SubscribeForm from './Subscribe';
 
 const WidgetDrawerTabs = () => {
@@ -41,6 +42,9 @@ const WidgetDrawerTabs = () => {
                 <TabPanel sx={{ width: '100%' }} value={WidgetTabValues.VIDEO_FORM}>
                     <VideoForm />
                 </TabPanel>
+                <TabPanel sx={{ width: '100%' }} value={WidgetTabValues.TIMELINE_FORM}>
+                    <TimelineForm />
+                </TabPanel>
             </TabContext>
         </>
     );
diff --git a/met-web/src/components/engagement/form/EngagementWidgets/WidgetOptionCards.tsx b/met-web/src/components/engagement/form/EngagementWidgets/WidgetOptionCards.tsx
index 0870513dc..4fbfcf768 100644
--- a/met-web/src/components/engagement/form/EngagementWidgets/WidgetOptionCards.tsx
+++ b/met-web/src/components/engagement/form/EngagementWidgets/WidgetOptionCards.tsx
@@ -8,6 +8,7 @@ import SubscribeOptionCard from './Subscribe/SubscribeOptionCard';
 import EventsOptionCard from './Events/EventsOptionCard';
 import MapOptionCard from './Map/MapOptionCard';
 import VideoOptionCard from './Video/VideoOptionCard';
+import TimelineOptionCard from './Timeline/TimelineOptionCard';
 
 const WidgetOptionCards = () => {
     return (
@@ -37,6 +38,9 @@ const WidgetOptionCards = () => {
             <Grid item xs={12} lg={6}>
                 <VideoOptionCard />
             </Grid>
+            <Grid item xs={12} lg={6}>
+                <TimelineOptionCard />
+            </Grid>
         </Grid>
     );
 };
diff --git a/met-web/src/components/engagement/form/EngagementWidgets/type.tsx b/met-web/src/components/engagement/form/EngagementWidgets/type.tsx
index 0a37d17c9..d66741f67 100644
--- a/met-web/src/components/engagement/form/EngagementWidgets/type.tsx
+++ b/met-web/src/components/engagement/form/EngagementWidgets/type.tsx
@@ -8,4 +8,5 @@ export const WidgetTabValues = {
     EVENTS_FORM: 'EVENTS_FORM',
     MAP_FORM: 'MAP_FORM',
     VIDEO_FORM: 'VIDEO_FORM',
+    TIMELINE_FORM: 'TIMELINE_FORM',
 };
diff --git a/met-web/src/components/engagement/view/widgets/Timeline/TimelineWidgetView.tsx b/met-web/src/components/engagement/view/widgets/Timeline/TimelineWidgetView.tsx
new file mode 100644
index 000000000..7ea5e882d
--- /dev/null
+++ b/met-web/src/components/engagement/view/widgets/Timeline/TimelineWidgetView.tsx
@@ -0,0 +1,149 @@
+import React, { useEffect, useState } from 'react';
+import { MetPaper, MetHeader2, MetParagraph, MetHeader4 } from 'components/common';
+import { Grid, Skeleton, Divider } from '@mui/material';
+import { Widget } from 'models/widget';
+import { useAppDispatch } from 'hooks';
+import { openNotification } from 'services/notificationService/notificationSlice';
+import { TimelineWidget, TimelineEvent } from 'models/timelineWidget';
+import { fetchTimelineWidgets } from 'services/widgetService/TimelineService';
+
+interface TimelineWidgetProps {
+    widget: Widget;
+}
+
+const TimelineWidgetView = ({ widget }: TimelineWidgetProps) => {
+    const dispatch = useAppDispatch();
+    const [timelineWidget, setTimelineWidget] = useState<TimelineWidget>({
+        id: 0,
+        widget_id: 0,
+        engagement_id: 0,
+        title: '',
+        description: '',
+        events: [],
+    });
+    const [isLoading, setIsLoading] = useState(true);
+
+    const fetchTimeline = async () => {
+        try {
+            const timelines = await fetchTimelineWidgets(widget.id);
+            const timeline = timelines[timelines.length - 1];
+            timeline.events.sort((a, b) => a.position - b.position);
+            setTimelineWidget(timeline);
+            setIsLoading(false);
+        } catch (error) {
+            setIsLoading(false);
+            console.log(error);
+            dispatch(
+                openNotification({
+                    severity: 'error',
+                    text: 'Error occurred while fetching Engagement widgets information',
+                }),
+            );
+        }
+    };
+
+    useEffect(() => {
+        fetchTimeline();
+    }, [widget]);
+
+    const handleRenderTimelineEvent = (tEvent: TimelineEvent, index: number) => {
+        const containerStylesObject = {
+            minHeight: index + 1 === timelineWidget.events.length ? '60px' : '80px',
+            display: 'flex',
+            flexDirection: 'row',
+            marginLeft: index + 1 === timelineWidget.events.length ? '24px' : '22px',
+            borderLeft: index + 1 === timelineWidget.events.length ? 'none' : '2px solid grey',
+        };
+        const circleContainerStylesObject = {
+            padding: '2px',
+            border: '2px solid grey',
+            marginLeft: '-25px',
+            display: 'inline-flex',
+            backgroundColor: '#fff',
+            borderRadius: '50%',
+            height: '48px',
+            width: '48px',
+        };
+        const circleStylesObject = {
+            width: '40px',
+            height: '40px',
+            borderRadius: '50%',
+            content: '""',
+            border: 1 !== tEvent.status ? '20px solid' : 'none',
+            borderColor: 3 === tEvent.status ? '#2e8540' : '#036',
+        };
+        const checkmarkStylesObject = {
+            marginLeft: '-12px',
+            marginTop: '-23px',
+            fontSize: '31px',
+            color: '#fff',
+        };
+        return (
+            <Grid item xs={12} sx={containerStylesObject} key={'event' + (index + 1)}>
+                <Grid item sx={circleContainerStylesObject}>
+                    <Grid item sx={circleStylesObject}>
+                        {3 == tEvent.status && (
+                            <Grid sx={checkmarkStylesObject} item>
+                                ✓
+                            </Grid>
+                        )}
+                    </Grid>
+                </Grid>
+                <Grid item sx={{ paddingLeft: '10px' }}>
+                    <MetHeader4 bold>{tEvent.description}</MetHeader4>
+                    <MetParagraph style={{ paddingBottom: index + 1 === timelineWidget.events.length ? '0' : '20px' }}>
+                        {tEvent.time}
+                    </MetParagraph>
+                </Grid>
+            </Grid>
+        );
+    };
+
+    if (isLoading) {
+        return (
+            <MetPaper elevation={1} sx={{ padding: '1em' }}>
+                <Grid container justifyContent="flex-start" spacing={3}>
+                    <Grid item xs={12}>
+                        <MetHeader2>
+                            <Skeleton variant="rectangular" />
+                        </MetHeader2>
+                    </Grid>
+                    <Grid item xs={12}>
+                        <Skeleton variant="rectangular" height="20em" />
+                    </Grid>
+                </Grid>
+            </MetPaper>
+        );
+    }
+
+    if (!timelineWidget) {
+        return null;
+    }
+
+    return (
+        <MetPaper elevation={1} sx={{ paddingTop: '0.5em', padding: '1em' }}>
+            <Grid container justifyContent={{ xs: 'center' }} alignItems="center" rowSpacing={2}>
+                <Grid
+                    item
+                    container
+                    justifyContent={{ xs: 'center', md: 'flex-start' }}
+                    flexDirection={'column'}
+                    xs={12}
+                    paddingBottom={0}
+                >
+                    <MetHeader2 bold>{timelineWidget.title}</MetHeader2>
+                    <Divider sx={{ borderWidth: 1, marginTop: 0.5 }} />
+                </Grid>
+                <Grid item xs={12}>
+                    <MetParagraph>{timelineWidget.description}</MetParagraph>
+                </Grid>
+                <Grid item xs={12}>
+                    {timelineWidget &&
+                        timelineWidget.events.map((tEvent, index) => handleRenderTimelineEvent(tEvent, index))}
+                </Grid>
+            </Grid>
+        </MetPaper>
+    );
+};
+
+export default TimelineWidgetView;
diff --git a/met-web/src/components/engagement/view/widgets/WidgetSwitch.tsx b/met-web/src/components/engagement/view/widgets/WidgetSwitch.tsx
index 0924ffa79..2e1391cad 100644
--- a/met-web/src/components/engagement/view/widgets/WidgetSwitch.tsx
+++ b/met-web/src/components/engagement/view/widgets/WidgetSwitch.tsx
@@ -7,6 +7,7 @@ import SubscribeWidget from './Subscribe/SubscribeWidget';
 import EventsWidget from './Events/EventsWidget';
 import MapWidget from './Map/MapWidget';
 import VideoWidgetView from './Video/VideoWidgetView';
+import TimelineWidgetView from './Timeline/TimelineWidgetView';
 interface WidgetSwitchProps {
     widget: Widget;
 }
@@ -33,6 +34,9 @@ export const WidgetSwitch = ({ widget }: WidgetSwitchProps) => {
                 <Case condition={widget.widget_type_id === WidgetType.Video}>
                     <VideoWidgetView widget={widget} />
                 </Case>
+                <Case condition={widget.widget_type_id === WidgetType.Timeline}>
+                    <TimelineWidgetView widget={widget} />
+                </Case>
             </Switch>
         </>
     );
diff --git a/met-web/src/models/timelineWidget.tsx b/met-web/src/models/timelineWidget.tsx
new file mode 100644
index 000000000..943d16820
--- /dev/null
+++ b/met-web/src/models/timelineWidget.tsx
@@ -0,0 +1,25 @@
+export interface TimelineWidget {
+    id: number;
+    engagement_id: number;
+    widget_id: number;
+    title: string;
+    description: string;
+    events: TimelineEvent[];
+}
+
+export interface TimelineEvent {
+    id: number;
+    engagement_id: number;
+    widget_id: number;
+    timeline_id: number;
+    description: string;
+    time: string;
+    status: EventStatus;
+    position: number;
+}
+
+export enum EventStatus {
+    Pending = 1,
+    InProgress = 2,
+    Completed = 3,
+}
diff --git a/met-web/src/models/widget.tsx b/met-web/src/models/widget.tsx
index 32e4cd3e4..529c072e2 100644
--- a/met-web/src/models/widget.tsx
+++ b/met-web/src/models/widget.tsx
@@ -22,4 +22,5 @@ export enum WidgetType {
     Map = 6,
     Video = 7,
     CACForm = 8,
+    Timeline = 9,
 }
diff --git a/met-web/src/services/widgetService/TimelineService/index.tsx b/met-web/src/services/widgetService/TimelineService/index.tsx
new file mode 100644
index 000000000..e90cd0b84
--- /dev/null
+++ b/met-web/src/services/widgetService/TimelineService/index.tsx
@@ -0,0 +1,58 @@
+import http from 'apiManager/httpRequestHandler';
+import Endpoints from 'apiManager/endpoints';
+import { replaceAllInURL, replaceUrl } from 'helper';
+import { TimelineWidget, TimelineEvent } from 'models/timelineWidget';
+
+interface PostTimelineRequest {
+    widget_id: number;
+    engagement_id: number;
+    title: string;
+    description: string;
+    events: TimelineEvent[];
+}
+
+interface PatchTimelineRequest {
+    events?: TimelineEvent[];
+    title?: string;
+    description?: string;
+}
+
+export const postTimeline = async (widget_id: number, data: PostTimelineRequest): Promise<TimelineWidget> => {
+    try {
+        const url = replaceUrl(Endpoints.TimelineWidgets.CREATE, 'widget_id', String(widget_id));
+        const response = await http.PostRequest<TimelineWidget>(url, data);
+        return response.data || Promise.reject('Failed to create timeline widget');
+    } catch (err) {
+        return Promise.reject(err);
+    }
+};
+
+export const patchTimeline = async (
+    widget_id: number,
+    timeline_id: number,
+    data: PatchTimelineRequest,
+): Promise<TimelineWidget> => {
+    try {
+        const url = replaceAllInURL({
+            URL: Endpoints.TimelineWidgets.UPDATE,
+            params: {
+                widget_id: String(widget_id),
+                timeline_id: String(timeline_id),
+            },
+        });
+        const response = await http.PatchRequest<TimelineWidget>(url, data);
+        return response.data || Promise.reject('Failed to update timeline widget');
+    } catch (err) {
+        return Promise.reject(err);
+    }
+};
+
+export const fetchTimelineWidgets = async (widget_id: number): Promise<TimelineWidget[]> => {
+    try {
+        const url = replaceUrl(Endpoints.TimelineWidgets.GET, 'widget_id', String(widget_id));
+        const responseData = await http.GetRequest<TimelineWidget[]>(url);
+        return responseData.data ?? [];
+    } catch (err) {
+        return Promise.reject(err);
+    }
+};