JuneAI studies your actual process, the spreadsheets, the approvals, the manual back-and-forth, and builds the tool that runs it. Quote generators, process automators, dashboards. Working in days.
import osimport reimport sysimport jsonimport tracebackfrom dataclasses import dataclass, fieldfrom datetime import datetime, timefrom pathlib import Pathfrom typing import Any, Dict, List, Optionalfrom flask import Flask, request, render_template_stringfrom openpyxl import Workbookfrom openpyxl.styles import Font, Alignmentfrom openpyxl.worksheet.table import Table, TableStyleInfotry:import pythoncomimport win32com.clientexcept ImportError:pythoncom = Nonewin32com = NoneAPP_PORT = 5055WORKFLOW_FILE = "saved_workflows.json"# -----------------------------# Utilities# -----------------------------def ensure_windows_outlook():if os.name != "nt":raise RuntimeError("This app only works on Windows with Outlook Desktop installed.")if pythoncom is None or win32com is None:raise RuntimeError("Missing pywin32. Install with: pip install pywin32")def safe_filename(value: str, max_len: int = 120) -> str:value = value or "No Subject"value = re.sub(r'[<>:"/\\|?*\r\n\t]+', "_", value)value = re.sub(r"\s+", " ", value).strip()return value[:max_len].strip(" ._") or "No Subject"def unique_path(path: Path) -> Path:if not path.exists():return pathstem = path.stemsuffix = path.suffixparent = path.parentcounter = 2while True:new_path = parent / f"{stem}_{counter}{suffix}"if not new_path.exists():return new_pathcounter += 1def parse_date(value: str, end_of_day: bool = False) -> Optional[datetime]:value = (value or "").strip()if not value:return Noned = datetime.strptime(value, "%Y-%m-%d").date()if end_of_day:return datetime.combine(d, time(23, 59, 59))return datetime.combine(d, time(0, 0, 0))def com_time_to_datetime(value: Any) -> Optional[datetime]:if not value:return Noneif isinstance(value, datetime):return value.replace(tzinfo=None)try:return datetime.fromtimestamp(value.timestamp()).replace(tzinfo=None)except Exception:passtry:text = str(value)return datetime.strptime(text[:19], "%Y-%m-%d %H:%M:%S")except Exception:return Nonedef get_smtp_sender(mail_item) -> str:try:sender_email = str(getattr(mail_item, "SenderEmailAddress", "") or "")except Exception:sender_email = ""if sender_email and not sender_email.startswith("/O="):return sender_emailtry:sender = mail_item.Senderexchange_user = sender.GetExchangeUser()if exchange_user:smtp = exchange_user.PrimarySmtpAddressif smtp:return smtpexcept Exception:passreturn sender_emaildef get_outlook_namespace():ensure_windows_outlook()pythoncom.CoInitialize()outlook = win32com.client.Dispatch("Outlook.Application")namespace = outlook.GetNamespace("MAPI")return namespacedef get_child_folder(parent_folder, child_name: str):child_name_clean = child_name.strip().lower()for i in range(1, parent_folder.Folders.Count + 1):folder = parent_folder.Folders.Item(i)if folder.Name.strip().lower() == child_name_clean:return folderavailable = [parent_folder.Folders.Item(i).Namefor i in range(1, parent_folder.Folders.Count + 1)]raise RuntimeError(f"Folder '{child_name}' not found under '{parent_folder.Name}'. "f"Available folders: {available}")def get_outlook_folder(namespace, folder_path: str):folder_path = (folder_path or "Inbox").strip()parts = [p.strip() for p in re.split(r"[\\/]+", folder_path) if p.strip()]if not parts:return namespace.GetDefaultFolder(6)default_folders = {"inbox": 6,"sent": 5,"sent items": 5,"drafts": 16,"deleted": 3,"deleted items": 3,"outbox": 4,"junk": 23,"junk email": 23,}first = parts[0].lower()if first in default_folders:current = namespace.GetDefaultFolder(default_folders[first])parts = parts[1:]else:current = Nonefor i in range(1, namespace.Folders.Count + 1):root = namespace.Folders.Item(i)if root.Name.strip().lower() == first:current = rootparts = parts[1:]breakif current is None:current = namespace.GetDefaultFolder(6)for part in parts:current = get_child_folder(current, part)return current# -----------------------------# Data models# -----------------------------@dataclassclass WorkflowConfig:outlook_folder: stroutput_dir: strdate_from: Optional[str] = Nonedate_to: Optional[str] = Nonesubject_contains: str = ""sender_contains: str = ""max_results: int = 100max_scan: int = 1000save_attachments: bool = False@dataclassclass EmailRecord:subject: strsender_name: strsender_email: strto: strcc: strreceived_time: Optional[datetime]sent_time: Optional[datetime]size: intcategories: strimportance: strhas_attachments: boolattachment_count: intconversation_id: strentry_id: strbody_preview: strmsg_path: str = ""attachments_dir: str = ""outlook_item: Any = field(default=None, repr=False)@dataclassclass WorkflowResult:started_at: datetimecompleted_at: Optional[datetime] = Nonetotal_found: int = 0msg_folder: str = ""excel_path: str = ""errors: List[str] = field(default_factory=list)# -----------------------------# Workflow Nodes# -----------------------------class Node:name = "Base Node"def run(self, context: Dict[str, Any]) -> Dict[str, Any]:raise NotImplementedErrorclass OutlookReadNode(Node):name = "Read Outlook Emails"def run(self, context: Dict[str, Any]) -> Dict[str, Any]:config: WorkflowConfig = context["config"]date_from = parse_date(config.date_from, end_of_day=False)date_to = parse_date(config.date_to, end_of_day=True)subject_filter = (config.subject_contains or "").lower().strip()sender_filter = (config.sender_contains or "").lower().strip()namespace = get_outlook_namespace()folder = get_outlook_folder(namespace, config.outlook_folder)items = folder.Itemsitems.Sort("[ReceivedTime]", True)records: List[EmailRecord] = []scanned = 0for item in items:scanned += 1if scanned > config.max_scan:breaktry:if getattr(item, "Class", None) != 43:continuereceived = com_time_to_datetime(getattr(item, "ReceivedTime", None))if date_from and received and received < date_from:continueif date_to and received and received > date_to:continuesubject = str(getattr(item, "Subject", "") or "")sender_name = str(getattr(item, "SenderName", "") or "")sender_email = get_smtp_sender(item)if subject_filter and subject_filter not in subject.lower():continuesender_match_text = f"{sender_name} {sender_email}".lower()if sender_filter and sender_filter not in sender_match_text:continueattachments = getattr(item, "Attachments", None)attachment_count = 0try:attachment_count = attachments.Count if attachments else 0except Exception:attachment_count = 0body = str(getattr(item, "Body", "") or "")body_preview = body.replace("\r", " ").replace("\n", " ")body_preview = re.sub(r"\s+", " ", body_preview).strip()[:500]record = EmailRecord(subject=subject,sender_name=sender_name,sender_email=sender_email,to=str(getattr(item, "To", "") or ""),cc=str(getattr(item, "CC", "") or ""),received_time=received,sent_time=com_time_to_datetime(getattr(item, "SentOn", None)),size=int(getattr(item, "Size", 0) or 0),categories=str(getattr(item, "Categories", "") or ""),importance=str(getattr(item, "Importance", "") or ""),has_attachments=attachment_count > 0,attachment_count=attachment_count,conversation_id=str(getattr(item, "ConversationID", "") or ""),entry_id=str(getattr(item, "EntryID", "") or ""),body_preview=body_preview,outlook_item=item,)records.append(record)if len(records) >= config.max_results:breakexcept Exception as e:context["result"].errors.append(f"Skipped one email because of error: {e}")context["emails"] = recordscontext["result"].total_found = len(records)return contextclass SaveMessagesNode(Node):name = "Save Emails as MSG"def run(self, context: Dict[str, Any]) -> Dict[str, Any]:config: WorkflowConfig = context["config"]records: List[EmailRecord] = context["emails"]output_dir = Path(config.output_dir).expanduser().resolve()run_stamp = datetime.now().strftime("%Y%m%d_%H%M%S")msg_folder = output_dir / f"outlook_email_export_{run_stamp}" / "emails_msg"attachments_root = output_dir / f"outlook_email_export_{run_stamp}" / "attachments"msg_folder.mkdir(parents=True, exist_ok=True)if config.save_attachments:attachments_root.mkdir(parents=True, exist_ok=True)for idx, record in enumerate(records, start=1):try:received_label = (record.received_time.strftime("%Y-%m-%d_%H-%M-%S")if record.received_timeelse "NoDate")base_name = safe_filename(f"{idx:04d}_{received_label}_{record.sender_name}_{record.subject}",max_len=180,)msg_path = unique_path(msg_folder / f"{base_name}.msg")record.outlook_item.SaveAs(str(msg_path), 9)record.msg_path = str(msg_path)if config.save_attachments and record.attachment_count > 0:email_attachment_dir = attachments_root / base_nameemail_attachment_dir.mkdir(parents=True, exist_ok=True)attachments = record.outlook_item.Attachmentsfor a_idx in range(1, attachments.Count + 1):attachment = attachments.Item(a_idx)original_name = str(attachment.FileName or f"attachment_{a_idx}")attachment_name = safe_filename(original_name, max_len=160)attachment_path = unique_path(email_attachment_dir / attachment_name)attachment.SaveAsFile(str(attachment_path))record.attachments_dir = str(email_attachment_dir)except Exception as e:context["result"].errors.append(f"Could not save email '{record.subject}': {e}")context["result"].msg_folder = str(msg_folder)return contextclass ExcelExportNode(Node):name = "Create Excel Report"def run(self, context: Dict[str, Any]) -> Dict[str, Any]:config: WorkflowConfig = context["config"]records: List[EmailRecord] = context["emails"]output_dir = Path(config.output_dir).expanduser().resolve()run_folder = Path(context["result"].msg_folder).parentexcel_path = unique_path(run_folder / "outlook_emails_export.xlsx")wb = Workbook()ws = wb.activews.title = "Outlook Emails"headers = ["Subject", "Sender Name", "Sender Email", "To", "CC","Received Time", "Sent Time", "Size Bytes", "Categories","Importance", "Has Attachments", "Attachment Count","Conversation ID", "Entry ID", "Saved MSG Path","Attachments Folder", "Body Preview",]ws.append(headers)for cell in ws[1]:cell.font = Font(bold=True)cell.alignment = Alignment(vertical="center")for record in records:ws.append([record.subject, record.sender_name, record.sender_email,record.to, record.cc,record.received_time.strftime("%Y-%m-%d %H:%M:%S") if record.received_time else "",record.sent_time.strftime("%Y-%m-%d %H:%M:%S") if record.sent_time else "",record.size, record.categories, record.importance,"Yes" if record.has_attachments else "No",record.attachment_count, record.conversation_id,record.entry_id, record.msg_path, record.attachments_dir,record.body_preview,])ws.freeze_panes = "A2"ws.auto_filter.ref = ws.dimensionsif ws.max_row > 1:table_ref = f"A1:Q{ws.max_row}"table = Table(displayName="OutlookEmailExport", ref=table_ref)style = TableStyleInfo(name="TableStyleMedium2",showRowStripes=True,)table.tableStyleInfo = stylews.add_table(table)wb.save(excel_path)context["result"].excel_path = str(excel_path)return context# -----------------------------# Workflow Engine# -----------------------------class Workflow:def __init__(self, config: WorkflowConfig):self.config = configself.nodes = [OutlookReadNode(),SaveMessagesNode(),ExcelExportNode(),]def run(self) -> WorkflowResult:result = WorkflowResult(started_at=datetime.now())context = {"config": self.config,"emails": [],"result": result,}try:for node in self.nodes:context = node.run(context)except Exception:result.errors.append(traceback.format_exc())result.completed_at = datetime.now()return resultapp = Flask(__name__)@app.route("/", methods=["GET"])def index():return render_template_string(HTML, form=default_form(), result=None)@app.route("/run", methods=["POST"])def run_workflow():config = form_to_config(request.form)workflow = Workflow(config)result = workflow.run()return render_template_string(HTML, form=default_form(), result=result)if __name__ == "__main__":print(f"Starting Outlook Workflow App on http://127.0.0.1:{APP_PORT}")app.run(host="127.0.0.1", port=APP_PORT, debug=False)
import osimport reimport sysimport jsonimport tracebackfrom dataclasses import dataclass, fieldfrom datetime import datetime, timefrom pathlib import Pathfrom typing import Any, Dict, List, Optionalfrom flask import Flask, request, render_template_stringfrom openpyxl import Workbookfrom openpyxl.styles import Font, Alignmentfrom openpyxl.worksheet.table import Table, TableStyleInfotry:import pythoncomimport win32com.clientexcept ImportError:pythoncom = Nonewin32com = NoneAPP_PORT = 5055WORKFLOW_FILE = "saved_workflows.json"# -----------------------------# Utilities# -----------------------------def ensure_windows_outlook():if os.name != "nt":raise RuntimeError("This app only works on Windows with Outlook Desktop installed.")if pythoncom is None or win32com is None:raise RuntimeError("Missing pywin32. Install with: pip install pywin32")def safe_filename(value: str, max_len: int = 120) -> str:value = value or "No Subject"value = re.sub(r'[<>:"/\\|?*\r\n\t]+', "_", value)value = re.sub(r"\s+", " ", value).strip()return value[:max_len].strip(" ._") or "No Subject"def unique_path(path: Path) -> Path:if not path.exists():return pathstem = path.stemsuffix = path.suffixparent = path.parentcounter = 2while True:new_path = parent / f"{stem}_{counter}{suffix}"if not new_path.exists():return new_pathcounter += 1def parse_date(value: str, end_of_day: bool = False) -> Optional[datetime]:value = (value or "").strip()if not value:return Noned = datetime.strptime(value, "%Y-%m-%d").date()if end_of_day:return datetime.combine(d, time(23, 59, 59))return datetime.combine(d, time(0, 0, 0))def com_time_to_datetime(value: Any) -> Optional[datetime]:if not value:return Noneif isinstance(value, datetime):return value.replace(tzinfo=None)try:return datetime.fromtimestamp(value.timestamp()).replace(tzinfo=None)except Exception:passtry:text = str(value)return datetime.strptime(text[:19], "%Y-%m-%d %H:%M:%S")except Exception:return Nonedef get_smtp_sender(mail_item) -> str:try:sender_email = str(getattr(mail_item, "SenderEmailAddress", "") or "")except Exception:sender_email = ""if sender_email and not sender_email.startswith("/O="):return sender_emailtry:sender = mail_item.Senderexchange_user = sender.GetExchangeUser()if exchange_user:smtp = exchange_user.PrimarySmtpAddressif smtp:return smtpexcept Exception:passreturn sender_emaildef get_outlook_namespace():ensure_windows_outlook()pythoncom.CoInitialize()outlook = win32com.client.Dispatch("Outlook.Application")namespace = outlook.GetNamespace("MAPI")return namespacedef get_child_folder(parent_folder, child_name: str):child_name_clean = child_name.strip().lower()for i in range(1, parent_folder.Folders.Count + 1):folder = parent_folder.Folders.Item(i)if folder.Name.strip().lower() == child_name_clean:return folderavailable = [parent_folder.Folders.Item(i).Namefor i in range(1, parent_folder.Folders.Count + 1)]raise RuntimeError(f"Folder '{child_name}' not found under '{parent_folder.Name}'. "f"Available folders: {available}")def get_outlook_folder(namespace, folder_path: str):folder_path = (folder_path or "Inbox").strip()parts = [p.strip() for p in re.split(r"[\\/]+", folder_path) if p.strip()]if not parts:return namespace.GetDefaultFolder(6)default_folders = {"inbox": 6,"sent": 5,"sent items": 5,"drafts": 16,"deleted": 3,"deleted items": 3,"outbox": 4,"junk": 23,"junk email": 23,}first = parts[0].lower()if first in default_folders:current = namespace.GetDefaultFolder(default_folders[first])parts = parts[1:]else:current = Nonefor i in range(1, namespace.Folders.Count + 1):root = namespace.Folders.Item(i)if root.Name.strip().lower() == first:current = rootparts = parts[1:]breakif current is None:current = namespace.GetDefaultFolder(6)for part in parts:current = get_child_folder(current, part)return current# -----------------------------# Data models# -----------------------------@dataclassclass WorkflowConfig:outlook_folder: stroutput_dir: strdate_from: Optional[str] = Nonedate_to: Optional[str] = Nonesubject_contains: str = ""sender_contains: str = ""max_results: int = 100max_scan: int = 1000save_attachments: bool = False@dataclassclass EmailRecord:subject: strsender_name: strsender_email: strto: strcc: strreceived_time: Optional[datetime]sent_time: Optional[datetime]size: intcategories: strimportance: strhas_attachments: boolattachment_count: intconversation_id: strentry_id: strbody_preview: strmsg_path: str = ""attachments_dir: str = ""outlook_item: Any = field(default=None, repr=False)@dataclassclass WorkflowResult:started_at: datetimecompleted_at: Optional[datetime] = Nonetotal_found: int = 0msg_folder: str = ""excel_path: str = ""errors: List[str] = field(default_factory=list)# -----------------------------# Workflow Nodes# -----------------------------class Node:name = "Base Node"def run(self, context: Dict[str, Any]) -> Dict[str, Any]:raise NotImplementedErrorclass OutlookReadNode(Node):name = "Read Outlook Emails"def run(self, context: Dict[str, Any]) -> Dict[str, Any]:config: WorkflowConfig = context["config"]date_from = parse_date(config.date_from, end_of_day=False)date_to = parse_date(config.date_to, end_of_day=True)subject_filter = (config.subject_contains or "").lower().strip()sender_filter = (config.sender_contains or "").lower().strip()namespace = get_outlook_namespace()folder = get_outlook_folder(namespace, config.outlook_folder)items = folder.Itemsitems.Sort("[ReceivedTime]", True)records: List[EmailRecord] = []scanned = 0for item in items:scanned += 1if scanned > config.max_scan:breaktry:if getattr(item, "Class", None) != 43:continuereceived = com_time_to_datetime(getattr(item, "ReceivedTime", None))if date_from and received and received < date_from:continueif date_to and received and received > date_to:continuesubject = str(getattr(item, "Subject", "") or "")sender_name = str(getattr(item, "SenderName", "") or "")sender_email = get_smtp_sender(item)if subject_filter and subject_filter not in subject.lower():continuesender_match_text = f"{sender_name} {sender_email}".lower()if sender_filter and sender_filter not in sender_match_text:continueattachments = getattr(item, "Attachments", None)attachment_count = 0try:attachment_count = attachments.Count if attachments else 0except Exception:attachment_count = 0body = str(getattr(item, "Body", "") or "")body_preview = body.replace("\r", " ").replace("\n", " ")body_preview = re.sub(r"\s+", " ", body_preview).strip()[:500]record = EmailRecord(subject=subject,sender_name=sender_name,sender_email=sender_email,to=str(getattr(item, "To", "") or ""),cc=str(getattr(item, "CC", "") or ""),received_time=received,sent_time=com_time_to_datetime(getattr(item, "SentOn", None)),size=int(getattr(item, "Size", 0) or 0),categories=str(getattr(item, "Categories", "") or ""),importance=str(getattr(item, "Importance", "") or ""),has_attachments=attachment_count > 0,attachment_count=attachment_count,conversation_id=str(getattr(item, "ConversationID", "") or ""),entry_id=str(getattr(item, "EntryID", "") or ""),body_preview=body_preview,outlook_item=item,)records.append(record)if len(records) >= config.max_results:breakexcept Exception as e:context["result"].errors.append(f"Skipped one email because of error: {e}")context["emails"] = recordscontext["result"].total_found = len(records)return contextclass SaveMessagesNode(Node):name = "Save Emails as MSG"def run(self, context: Dict[str, Any]) -> Dict[str, Any]:config: WorkflowConfig = context["config"]records: List[EmailRecord] = context["emails"]output_dir = Path(config.output_dir).expanduser().resolve()run_stamp = datetime.now().strftime("%Y%m%d_%H%M%S")msg_folder = output_dir / f"outlook_email_export_{run_stamp}" / "emails_msg"attachments_root = output_dir / f"outlook_email_export_{run_stamp}" / "attachments"msg_folder.mkdir(parents=True, exist_ok=True)if config.save_attachments:attachments_root.mkdir(parents=True, exist_ok=True)for idx, record in enumerate(records, start=1):try:received_label = (record.received_time.strftime("%Y-%m-%d_%H-%M-%S")if record.received_timeelse "NoDate")base_name = safe_filename(f"{idx:04d}_{received_label}_{record.sender_name}_{record.subject}",max_len=180,)msg_path = unique_path(msg_folder / f"{base_name}.msg")record.outlook_item.SaveAs(str(msg_path), 9)record.msg_path = str(msg_path)if config.save_attachments and record.attachment_count > 0:email_attachment_dir = attachments_root / base_nameemail_attachment_dir.mkdir(parents=True, exist_ok=True)attachments = record.outlook_item.Attachmentsfor a_idx in range(1, attachments.Count + 1):attachment = attachments.Item(a_idx)original_name = str(attachment.FileName or f"attachment_{a_idx}")attachment_name = safe_filename(original_name, max_len=160)attachment_path = unique_path(email_attachment_dir / attachment_name)attachment.SaveAsFile(str(attachment_path))record.attachments_dir = str(email_attachment_dir)except Exception as e:context["result"].errors.append(f"Could not save email '{record.subject}': {e}")context["result"].msg_folder = str(msg_folder)return contextclass ExcelExportNode(Node):name = "Create Excel Report"def run(self, context: Dict[str, Any]) -> Dict[str, Any]:config: WorkflowConfig = context["config"]records: List[EmailRecord] = context["emails"]output_dir = Path(config.output_dir).expanduser().resolve()run_folder = Path(context["result"].msg_folder).parentexcel_path = unique_path(run_folder / "outlook_emails_export.xlsx")wb = Workbook()ws = wb.activews.title = "Outlook Emails"headers = ["Subject", "Sender Name", "Sender Email", "To", "CC","Received Time", "Sent Time", "Size Bytes", "Categories","Importance", "Has Attachments", "Attachment Count","Conversation ID", "Entry ID", "Saved MSG Path","Attachments Folder", "Body Preview",]ws.append(headers)for cell in ws[1]:cell.font = Font(bold=True)cell.alignment = Alignment(vertical="center")for record in records:ws.append([record.subject, record.sender_name, record.sender_email,record.to, record.cc,record.received_time.strftime("%Y-%m-%d %H:%M:%S") if record.received_time else "",record.sent_time.strftime("%Y-%m-%d %H:%M:%S") if record.sent_time else "",record.size, record.categories, record.importance,"Yes" if record.has_attachments else "No",record.attachment_count, record.conversation_id,record.entry_id, record.msg_path, record.attachments_dir,record.body_preview,])ws.freeze_panes = "A2"ws.auto_filter.ref = ws.dimensionsif ws.max_row > 1:table_ref = f"A1:Q{ws.max_row}"table = Table(displayName="OutlookEmailExport", ref=table_ref)style = TableStyleInfo(name="TableStyleMedium2",showRowStripes=True,)table.tableStyleInfo = stylews.add_table(table)wb.save(excel_path)context["result"].excel_path = str(excel_path)return context# -----------------------------# Workflow Engine# -----------------------------class Workflow:def __init__(self, config: WorkflowConfig):self.config = configself.nodes = [OutlookReadNode(),SaveMessagesNode(),ExcelExportNode(),]def run(self) -> WorkflowResult:result = WorkflowResult(started_at=datetime.now())context = {"config": self.config,"emails": [],"result": result,}try:for node in self.nodes:context = node.run(context)except Exception:result.errors.append(traceback.format_exc())result.completed_at = datetime.now()return resultapp = Flask(__name__)@app.route("/", methods=["GET"])def index():return render_template_string(HTML, form=default_form(), result=None)@app.route("/run", methods=["POST"])def run_workflow():config = form_to_config(request.form)workflow = Workflow(config)result = workflow.run()return render_template_string(HTML, form=default_form(), result=result)if __name__ == "__main__":print(f"Starting Outlook Workflow App on http://127.0.0.1:{APP_PORT}")app.run(host="127.0.0.1", port=APP_PORT, debug=False)
Every capability ships inside the tool we deliver. Nothing theoretical, nothing you have to wire up yourself.
We map your existing workflow, forms, approvals, calculations, exceptions, and pin down exactly what to automate.
A workflow built around your business, not a template. Your terminology, your approval chain, your edge cases.
Quotes, reports, work orders and invoices generated straight from workflow data. Formatted, branded, ready to send.
Runs inside your own environment. No cloud dependency, no data leaving your network. Your process stays yours.
Bug fixes and updates as your process changes, plus direct support, included. Your tool never goes stale.
Walk us through how the work happens today, in spreadsheets, on paper, or a legacy system.
JuneAI maps your process, finds the inefficiencies, and designs the optimal software flow.
A custom tool is built around your exact workflow, web, desktop, or automation script.
Your tool and license live in your JuneAI workspace. Track status, get updates, request changes.
Hover any build to watch it run.




JuneAI reads from the tools your business already runs on, builds the workflow in the middle, and delivers the outputs back where you need them. No rip-and-replace.
Covers the tool, all updates as your workflow evolves, and direct support. Choose the commitment that suits you.
Launch price
Full access to your JuneAI-built tool with updates and support. Cancel anytime.
Most popular, lowest yet
For businesses that rely on their JuneAI tool daily. Priority support + custom update requests.
Multiple departments or a larger rollout? We map your workload and workflows first; licensing for the resulting tools is worked out once we know what's needed.
Start a projectTell us how your business actually runs, the spreadsheets, the manual back-and-forth. We'll build the tool that takes it off your plate.